mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eadf7bc474 | ||
|
|
00a326ef78 | ||
|
|
c09045fe59 | ||
|
|
5bd8ef01e5 | ||
|
|
3181ed2c5a | ||
|
|
6aed3b08f3 | ||
|
|
281467a345 | ||
|
|
c7e36986e7 | ||
|
|
d8576a2ed3 | ||
|
|
2b6c2b561c | ||
|
|
12c94111b5 | ||
|
|
7769156cfc | ||
|
|
0ecfdff4e7 | ||
|
|
6df5a6137f | ||
|
|
b3794f4e6a | ||
|
|
14d3901d30 | ||
|
|
d4766513e4 | ||
|
|
72dc7aa6aa | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 |
@@ -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
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: 10935bb5e4...c8036190fc
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
@@ -283,6 +284,10 @@ set(SOURCE_FILES
|
||||
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
|
||||
@@ -297,6 +302,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
|
||||
@@ -669,3 +675,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();
|
||||
|
||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||
// reconcile every runtime-gated advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -924,11 +926,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
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,
|
||||
@@ -955,6 +959,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
if (drawIndirectSupported) {
|
||||
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||
}
|
||||
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||
// that incomplete case.
|
||||
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||
|
||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -1431,6 +1431,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 +1462,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;
|
||||
}
|
||||
@@ -1600,7 +1605,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
||||
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
||||
|
||||
IntVec4 backendViewport = parameters.Viewport;
|
||||
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
||||
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -1614,7 +1619,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_syncedBackendViewport = backendViewport;
|
||||
}
|
||||
|
||||
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
|
||||
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
|
||||
// struct, i.e. in the tail span.
|
||||
if (tailSpanDirty) {
|
||||
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
||||
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||
@@ -1633,11 +1639,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
||||
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
||||
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
||||
SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST);
|
||||
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
||||
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
||||
|
||||
#undef SYNC_CAPABILITY
|
||||
|
||||
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
|
||||
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
|
||||
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
|
||||
// index every ES draw rasterizes against - can be forwarded; a program that
|
||||
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
|
||||
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
|
||||
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
|
||||
{
|
||||
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
const Bool syncedScissorTest =
|
||||
(g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
if (forceFullPush || scissorTest != syncedScissorTest) {
|
||||
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
||||
@@ -1864,8 +1885,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
|
||||
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2003,7 +2024,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
||||
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
||||
// loses the terrain and the whole GUI.
|
||||
IntVec4 backendScissorBox = parameters.ScissorBox;
|
||||
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
|
||||
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -2113,6 +2134,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);
|
||||
@@ -2296,6 +2326,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(
|
||||
@@ -3045,10 +3077,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
||||
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
|
||||
g_GLESFuncs.glDrawArraysIndirect != nullptr;
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
|
||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||
@@ -3937,8 +3966,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) {
|
||||
@@ -4084,13 +4124,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) {
|
||||
@@ -4762,7 +4814,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// restores the app state on exit, tracked via the render-state shadow.
|
||||
class ScopedScissorDisable {
|
||||
public:
|
||||
ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) {
|
||||
ScopedScissorDisable()
|
||||
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
|
||||
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
~ScopedScissorDisable() {
|
||||
@@ -5563,6 +5616,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
||||
//
|
||||
// The frontend hands this backend the target the APPLICATION named, and three of the
|
||||
// targets core GL has do not exist in ES at all. They are not missing here either - the
|
||||
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
|
||||
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
|
||||
// but glCopyImageSubData was the one path that never asked for that translation and passed
|
||||
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
|
||||
// and with the error only asserted on (asserts are compiled out of an INFO build) the
|
||||
// destination silently keeps whatever it held.
|
||||
//
|
||||
// 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.
|
||||
struct GLESCopyImageEndpoint {
|
||||
GLenum target = GL_TEXTURE_2D;
|
||||
GLint x = 0;
|
||||
GLint y = 0;
|
||||
GLint z = 0;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
endpoint.x = x;
|
||||
endpoint.y = y;
|
||||
endpoint.z = z;
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers
|
||||
// move, so at most one of the two can be a 1D array only in the degenerate single-layer
|
||||
// case, where the swap is the identity anyway.
|
||||
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) {
|
||||
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget);
|
||||
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget);
|
||||
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) {
|
||||
return;
|
||||
}
|
||||
std::swap(height, depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
@@ -5592,6 +5695,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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());
|
||||
@@ -5599,12 +5708,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
|
||||
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
||||
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight);
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5615,29 +5724,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError == GL_NO_ERROR) {
|
||||
return;
|
||||
}
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
|
||||
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY);
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
AssertNoGLError("glCopyImageSubData");
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
// Every error condition glCopyImageSubData has was already ruled out by the frontend
|
||||
// validator, so a driver error here is an internal invariant violation, not something
|
||||
// the application can provoke. Say so where an INFO build can still see it, then trap
|
||||
// in the builds that trap - the previous bare assert left a release build with a
|
||||
// destination that silently kept its old contents.
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
|
||||
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(src.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
||||
}
|
||||
}
|
||||
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
@@ -8220,6 +8343,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();
|
||||
@@ -8748,6 +8874,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;
|
||||
|
||||
@@ -577,6 +577,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 +732,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 +836,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 +967,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 +1650,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 +1701,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 +1824,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (indexBufferSynced) {
|
||||
m_syncedIndexBufferVersion = currentIndexBufferVersion;
|
||||
m_syncedIndexBufferObject = currentIndexBufferObject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1804,6 +1836,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (emitAttributes) {
|
||||
m_syncedFetchBaseInstance = fetchBaseInstance;
|
||||
}
|
||||
m_syncedBufferIdGeneration = currentBufferIdGeneration;
|
||||
}
|
||||
|
||||
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
|
||||
@@ -1941,6 +1974,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);
|
||||
}
|
||||
@@ -3512,6 +3550,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 +4041,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 +4138,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 +4176,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 +4805,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,7 +4859,7 @@ 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;
|
||||
}
|
||||
@@ -4804,7 +4869,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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 +4891,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 +4907,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 +4922,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 +4932,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 +4946,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 +4965,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 +4982,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;
|
||||
}
|
||||
|
||||
@@ -346,6 +346,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 +473,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 +493,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 +817,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 +911,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
|
||||
|
||||
@@ -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());
|
||||
@@ -497,20 +502,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
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_multi_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,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||
@@ -530,6 +537,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -678,6 +692,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -688,9 +705,16 @@ 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(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -924,6 +948,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;
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
@@ -406,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
|
||||
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
|
||||
@@ -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;
|
||||
@@ -1997,6 +2023,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
|
||||
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
|
||||
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
|
||||
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
@@ -2339,6 +2388,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
|
||||
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
|
||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||
// pipeline, and a false positive costs only a wider viewportCount.
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.writesViewportIndexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex]) continue;
|
||||
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||
|
||||
const auto& module = spirv[moduleIndex];
|
||||
if (module.empty()) continue;
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
|
||||
// silently collapse every viewport onto 0 (the exact bug this reflection exists
|
||||
// to fix); over-declaring costs one extra viewport slot on a program that never
|
||||
// uses it.
|
||||
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
|
||||
"program writes gl_ViewportIndex",
|
||||
static_cast<Int>(createResult));
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
}
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
@@ -2910,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),
|
||||
@@ -2991,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);
|
||||
|
||||
@@ -3031,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);
|
||||
}
|
||||
@@ -3049,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
|
||||
@@ -3073,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; "
|
||||
@@ -3091,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:
|
||||
@@ -3114,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",
|
||||
@@ -3138,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",
|
||||
@@ -3159,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
|
||||
@@ -3176,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);
|
||||
@@ -3189,6 +3437,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
#endif
|
||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||
ReflectLayout(program, moduleSpirvs, entry);
|
||||
@@ -3235,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;
|
||||
}
|
||||
@@ -3376,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
|
||||
@@ -151,6 +172,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
@@ -190,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);
|
||||
@@ -218,11 +246,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
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;
|
||||
@@ -234,6 +264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
@@ -248,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);
|
||||
@@ -276,11 +308,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
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;
|
||||
@@ -292,6 +326,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
@@ -337,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
|
||||
@@ -400,6 +462,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
@@ -434,6 +502,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
@@ -456,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);
|
||||
}
|
||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||
// and view lookups - would recompute the identical descriptor.
|
||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
||||
m_samplerResolveMemo[binding].infoValid) {
|
||||
m_samplerResolveMemo[binding].infoValid &&
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||
return true;
|
||||
}
|
||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (binding < m_samplerResolveMemo.size()) {
|
||||
if (descriptorMemoUsable) {
|
||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||
m_samplerResolveMemo[binding].infoValid = true;
|
||||
} else {
|
||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||
@@ -540,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;
|
||||
}
|
||||
@@ -1267,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 {
|
||||
@@ -1388,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;
|
||||
@@ -1431,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;
|
||||
@@ -1445,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",
|
||||
@@ -1472,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;
|
||||
@@ -1515,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;
|
||||
}
|
||||
@@ -1633,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
|
||||
@@ -1660,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() &&
|
||||
@@ -1884,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,
|
||||
@@ -341,8 +360,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.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, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||
|
||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
||||
oldResource.arrayLayers);
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||
|
||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||
@@ -1191,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool lowerTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
||||
resource.aspect, 0, writtenMipLevel);
|
||||
MOBILEGL_ASSERT(lowerTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1203,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool upperTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
||||
resource.arrayLayers);
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||
MOBILEGL_ASSERT(upperTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1257,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
@@ -1288,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1296,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));
|
||||
}
|
||||
@@ -1355,8 +1502,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 layerCount) {
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||
Uint32 levelCount) {
|
||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||
@@ -1381,7 +1528,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||
barrier.subresourceRange.levelCount = levelCount;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = layerCount;
|
||||
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -1840,6 +1993,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;
|
||||
}
|
||||
}
|
||||
@@ -1862,6 +2022,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
|
||||
@@ -2605,7 +2771,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
uploadSrcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
@@ -2709,7 +2875,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||
outResource.layout = finalLayout;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -388,12 +400,24 @@ public:
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||
//
|
||||
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||
// Nothing does, or can, do that per layer.
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
||||
Uint32 layerCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||
Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize, Uint8* outPixels);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -298,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||
}
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -536,7 +554,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.
|
||||
@@ -547,6 +578,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||
Bool m_multiViewportFeatureEnabled = false;
|
||||
Uint32 m_maxRasterizableViewports = 1;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
@@ -770,6 +807,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 = {};
|
||||
|
||||
@@ -807,6 +848,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;
|
||||
@@ -830,6 +876,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
@@ -897,6 +948,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;
|
||||
@@ -1023,6 +1076,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] = {};
|
||||
@@ -1116,11 +1177,34 @@ 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
|
||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||
// rasterizable viewport count, which takes the unmemoized array path.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||
Uint32 viewportCount = 1);
|
||||
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||
// disagree.
|
||||
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
@@ -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)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -27,6 +28,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||
// this file's blend-state queries name unqualified.
|
||||
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||
|
||||
namespace {
|
||||
enum class IndexedBufferQueryKind {
|
||||
Binding,
|
||||
@@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||
// index with viewport 0's value, which is what
|
||||
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||
// getter width funnels through this so they can never disagree with each other.
|
||||
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||
switch (target) {
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
out[0] = viewport.x();
|
||||
out[1] = viewport.y();
|
||||
out[2] = viewport.z();
|
||||
out[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_SCISSOR_BOX: {
|
||||
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||
out[0] = static_cast<GLfloat>(box.x());
|
||||
out[1] = static_cast<GLfloat>(box.y());
|
||||
out[2] = static_cast<GLfloat>(box.z());
|
||||
out[3] = static_cast<GLfloat>(box.w());
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_RANGE: {
|
||||
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
out[0] = range.x();
|
||||
out[1] = range.y();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||
static_cast<Uint32>(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||
params[0] = viewport.x();
|
||||
params[1] = viewport.y();
|
||||
params[2] = viewport.z();
|
||||
params[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
@@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi(target, index);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_DEPTH_RANGE: {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
@@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||
ReadIndexedViewportStateFloat(target, index, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLdouble>(values[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// frontend-only value simply is not in the driver's table.
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = static_cast<GLint64>(values[0]);
|
||||
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLint64>(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||
@@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.MaxViewportHeight;
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
*params = dynamicParameters.MaxViewports;
|
||||
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||
// carrying that device number for exactly that decision.
|
||||
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -648,4 +648,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
|
||||
|
||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
if (target != GL_BLEND) {
|
||||
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
GLuint limit = 0;
|
||||
const char* indexName = nullptr;
|
||||
switch (target) {
|
||||
case GL_BLEND:
|
||||
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||
indexName = "Buffer";
|
||||
break;
|
||||
case GL_SCISSOR_TEST:
|
||||
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||
indexName = "Viewport";
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Only GL_BLEND is supported for indexed capability state."));
|
||||
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||
"capability state."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||
if (index >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(indexName) + " index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||
"."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Viewport index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||
return false;
|
||||
}
|
||||
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"first (" + std::to_string(first) + ") + count (" +
|
||||
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||
if (width >= T(0) && height >= T(0)) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||
// error queue holds exactly one entry.
|
||||
template <typename T>
|
||||
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||
")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||
if (v != nullptr) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||
::MobileGL::BlendEquation& outEquation) {
|
||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array setters ------------------
|
||||
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||
}
|
||||
|
||||
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||
}
|
||||
|
||||
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
first + static_cast<GLuint>(i),
|
||||
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||
}
|
||||
}
|
||||
|
||||
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||
}
|
||||
|
||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
Bool applyFront = false;
|
||||
Bool applyBack = false;
|
||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||
}
|
||||
@@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||
return GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
||||
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||
? 4
|
||||
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
GLboolean IsEnabled_State(GLenum cap) {
|
||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Disablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Enablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Viewport_State(x, y, width, height);
|
||||
}
|
||||
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
ViewportArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
ViewportIndexedf_State(index, x, y, w, h);
|
||||
}
|
||||
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||
ScissorArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
ScissorIndexed_State(index, left, bottom, width, height);
|
||||
}
|
||||
|
||||
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
DepthRangeArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||
DepthRangeIndexed_State(index, n, f);
|
||||
}
|
||||
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Enablei(GLenum target, GLuint index);
|
||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||
|
||||
@@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
|
||||
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
|
||||
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
|
||||
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
|
||||
switch (target) {
|
||||
case GL_RENDERBUFFER:
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!mipmapTexture) return textureObject->GetBaseSize();
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
|
||||
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
|
||||
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
|
||||
// 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,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} name does not correspond to an existing image object.", endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
|
||||
// does not match the object INVALID_ENUM, where the shared uniformity helper records
|
||||
// INVALID_OPERATION for the upload paths that share it.
|
||||
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target, const char* endpointName) {
|
||||
if (!textureObject || textureObject->GetTarget() == target) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} target {} does not match the target the object was created with ({}).",
|
||||
endpointName, MG_Util::ConvertTextureTargetToString(target),
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
@@ -3396,8 +3468,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
@@ -3425,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
|
||||
// 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()) {
|
||||
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())));
|
||||
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()) {
|
||||
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())));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -5600,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
||||
srcWidth, srcHeight, 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
|
||||
// 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,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureTarget(TextureTarget target) {
|
||||
@@ -515,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||
if (unsizedFormat1 != unsizedFormat2) {
|
||||
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
||||
// std::format() call whose format string was the component name, so every
|
||||
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
||||
// hand over component/function/message separately.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||
CopyImageTexelBlock block{};
|
||||
if (compressedFormat != GL_NONE) {
|
||||
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||
if (info.blockByteSize != 0) {
|
||||
block.byteSize = info.blockByteSize;
|
||||
block.blockWidth = info.blockWidth;
|
||||
block.blockHeight = info.blockHeight;
|
||||
block.compressed = true;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||
// copy between them would in fact preserve the bytes.
|
||||
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||
return block;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock) {
|
||||
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
"A copied image has no storage whose texel size is known."));
|
||||
return false;
|
||||
}
|
||||
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||
"formats are not copy-compatible.",
|
||||
srcBlock.byteSize, dstBlock.byteSize)));
|
||||
return false;
|
||||
}
|
||||
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||
if (srcBlock.compressed && dstBlock.compressed &&
|
||||
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||
dstBlock.blockHeight)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||
if (!block.compressed) return true;
|
||||
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||
// whole even when the last block is partial.
|
||||
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||
if (originAligned && widthOk && heightOk) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||
"grid of a {} x {} image.",
|
||||
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||
|
||||
@@ -50,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||
struct CopyImageTexelBlock {
|
||||
SizeT byteSize = 0;
|
||||
Uint blockWidth = 1;
|
||||
Uint blockHeight = 1;
|
||||
Bool compressed = false;
|
||||
};
|
||||
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock);
|
||||
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||
// the edge of the image.
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||
// the requested internalformat asks for, but may supply more.
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -63,10 +68,14 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/ViewportArrayScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
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
|
||||
@@ -80,6 +89,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -90,9 +100,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
|
||||
@@ -101,6 +122,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
// different source, so all three are checked on both lanes.
|
||||
//
|
||||
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||
GLint maxViewports = -1;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||
"viewport, and the CTS sizes its arrays off this number";
|
||||
|
||||
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
|
||||
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||
// limit MobileGL currently passes through from the driver.
|
||||
GLint subpixelBits = -1;
|
||||
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||
"a negative value is what a sign-flipped uint32 looks like";
|
||||
|
||||
GLint viewportDims[2] = {-1, -1};
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint maxRenderbufferSize = -1;
|
||||
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,378 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A TRANSFER OFF A NON-ZERO ATTACHMENT LAYER READS THE LAYER THE BARRIER MOVED.
|
||||
//
|
||||
// Every transfer DirectVulkan performs against a framebuffer attachment is three commands: a
|
||||
// barrier that puts the image in TRANSFER_SRC/DST, the copy or blit itself, and a barrier that
|
||||
// puts it back. The copy names the attachment's layer - glFramebufferTextureLayer(.., layer) ends
|
||||
// up in `srcSubresource.baseArrayLayer` - but TransitionImageLayout used to emit `layerCount = 1`
|
||||
// from `baseArrayLayer 0`, so for every attachment on a layer above zero the barrier moved layer 0
|
||||
// and the copy read layer N. The layer the transfer touched was never transitioned: it sat in
|
||||
// COLOR_ATTACHMENT_OPTIMAL (or DEPTH_STENCIL_ATTACHMENT_OPTIMAL) while being read as TRANSFER_SRC.
|
||||
//
|
||||
// That is undefined behaviour, not a guaranteed wrong pixel: a layout is a compression/tiling
|
||||
// promise, so a driver that stores both layouts identically returns the right bytes anyway. The
|
||||
// software lanes (lavapipe) are exactly such a driver, which is why this scenario is paired with a
|
||||
// validation-layer run - the layer names the mismatch outright
|
||||
// (VUID-vkCmdCopyImageToBuffer-srcImageLayout-00189, "srcImageLayout ... doesn't match the actual
|
||||
// current layout") where the pixels here cannot. On a tiler that really does re-tile per layout,
|
||||
// these are the reads that come back as garbage.
|
||||
//
|
||||
// The four cases below are the four transfer paths that take an attachment layer from GL:
|
||||
//
|
||||
// glReadPixels (colour) -> VulkanRenderer::ReadPixels
|
||||
// glBlitFramebuffer (colour) -> VulkanRenderer::BlitNamedFramebuffer
|
||||
// glReadPixels (GL_DEPTH_COMPONENT) -> VulkanRenderer::ReadDepthStencilImageToClient
|
||||
// glBlitFramebuffer (GL_DEPTH_BUFFER_BIT) -> VulkanRenderer::BlitNamedFramebuffer, depth leg
|
||||
//
|
||||
// Each one renders or clears INTO the non-zero layer first, so the image is genuinely sitting in
|
||||
// its attachment layout when the transfer starts - a scenario that only uploaded texels would
|
||||
// leave it in a transfer layout already and the mismatched barrier would be a no-op.
|
||||
//
|
||||
// Every case also asserts the layers it did not name still hold their own fill, so a backend that
|
||||
// "fixed" the miss by transferring the whole image passes neither half.
|
||||
//
|
||||
// DirectGLES is the control: it hands the same calls to the driver, so a failure on both backends
|
||||
// means the scenario is wrong and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#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 int kWidth = 8;
|
||||
constexpr int kHeight = 8;
|
||||
// Four layers with the subject at index 2: layers on both sides of it stay untouched, so
|
||||
// "moved the whole image" and "moved layer 0" are both distinguishable from correct.
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kSubjectLayer = 2;
|
||||
|
||||
// A value no correct read can produce, so "the backend wrote nothing" fails loudly.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-layer fill, uniform within a layer: the defect is about WHICH layer is addressed, and
|
||||
// a value that also varied inside the layer would make the assertions depend on row order.
|
||||
Rgba8 LayerFill(int layer) {
|
||||
return {static_cast<GLubyte>(17 + layer * 30), static_cast<GLubyte>(200 - layer * 25),
|
||||
static_cast<GLubyte>(60 + layer * 40), 255};
|
||||
}
|
||||
|
||||
// What the draw paints - matches kFS below, and is deliberately none of the LayerFill
|
||||
// values so "the draw never landed" cannot read as a pass.
|
||||
constexpr Rgba8 kPaintedColor{26, 51, 204, 255};
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.1, 0.2, 0.8, 1.0); }
|
||||
)";
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
class LayeredAttachmentBarrierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_program != 0) {
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(m_program);
|
||||
m_program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// An RGBA8 2D array with a different uniform colour per layer.
|
||||
GLuint MakeColorArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
const std::vector<Rgba8> texels(static_cast<std::size_t>(kWidth) * kHeight, LayerFill(layer));
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kWidth, kHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A depth 2D array. No initial upload: depth arrays are filled by clearing through an
|
||||
// attachment, which is also the state the transfer paths have to cope with.
|
||||
GLuint MakeDepthArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One FBO naming `layer` of the given arrays. Depth is optional (0 = colour only).
|
||||
GLuint MakeLayerFbo(GLuint colorArray, GLuint depthArray, int layer) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorArray, 0, layer);
|
||||
if (depthArray != 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, layer);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "layer " << layer << " is not attachable";
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// glReadPixels of one whole layer, through an FBO that names it.
|
||||
Rgba8 ReadLayer(GLuint colorArray, int layer) {
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, layer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// The fill is uniform within a layer, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "layer " << layer << " is not uniform: texel 0 is " << Describe(pixels[0]) << ", texel "
|
||||
<< i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// Every layer but `changed` still holds its own fill.
|
||||
void ExpectOtherLayersUntouched(GLuint colorArray, int changed, const char* what) {
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
if (layer == changed) continue;
|
||||
const Rgba8 actual = ReadLayer(colorArray, layer);
|
||||
EXPECT_TRUE(actual == LayerFill(layer))
|
||||
<< what << ": layer " << layer << " should still hold its fill but is " << Describe(actual)
|
||||
<< ", expected " << Describe(LayerFill(layer));
|
||||
}
|
||||
}
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// glReadPixels straight off a layer that was just rendered to. The image is in
|
||||
// COLOR_ATTACHMENT_OPTIMAL when the readback barrier runs, so the barrier and the copy
|
||||
// disagreeing about the layer is a live layout mismatch, not a bookkeeping detail.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadPixelsOffRenderedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
for (std::size_t i = 0; i < pixels.size(); ++i) {
|
||||
ASSERT_NEAR(pixels[i].r, kPaintedColor.r, 2)
|
||||
<< "texel " << i << " of the rendered layer is " << Describe(pixels[i]);
|
||||
ASSERT_NEAR(pixels[i].g, kPaintedColor.g, 2) << "texel " << i;
|
||||
ASSERT_NEAR(pixels[i].b, kPaintedColor.b, 2) << "texel " << i;
|
||||
}
|
||||
|
||||
ExpectOtherLayersUntouched(colorArray, kSubjectLayer, "readback off a rendered layer");
|
||||
}
|
||||
|
||||
// glBlitFramebuffer between two non-zero layers of two different arrays. Both endpoints are
|
||||
// above layer 0, so the source and destination barriers are each wrong on their own side.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitBetweenNonZeroColorLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceArray = MakeColorArray();
|
||||
const GLuint destinationArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceArray, 0, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationArray, 0, kDestinationLayer);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Rgba8 blitted = ReadLayer(destinationArray, kDestinationLayer);
|
||||
EXPECT_NEAR(blitted.r, kPaintedColor.r, 2) << "blit destination layer is " << Describe(blitted);
|
||||
EXPECT_NEAR(blitted.g, kPaintedColor.g, 2);
|
||||
EXPECT_NEAR(blitted.b, kPaintedColor.b, 2);
|
||||
|
||||
ExpectOtherLayersUntouched(destinationArray, kDestinationLayer, "colour blit destination");
|
||||
// The source layer was rendered, not blitted into, so it is checked separately.
|
||||
const Rgba8 source = ReadLayer(sourceArray, kSourceLayer);
|
||||
EXPECT_NEAR(source.r, kPaintedColor.r, 2) << "blit source layer is " << Describe(source);
|
||||
ExpectOtherLayersUntouched(sourceArray, kSourceLayer, "colour blit source");
|
||||
}
|
||||
|
||||
// The depth aspect of the same readback path: the depth image sits in
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL after the clear, and the copy names the attached layer.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadDepthOffClearedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
const GLuint depthArray = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, depthArray, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.375);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.375f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) off layer " << kSubjectLayer << " returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
}
|
||||
|
||||
// The depth leg of the blit path, both endpoints above layer 0. Verified by reading the
|
||||
// destination's depth back, which is the same readback the case above pins - so a failure
|
||||
// here with that one passing is the blit, not the readback.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitDepthBetweenNonZeroLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceColor = MakeColorArray();
|
||||
const GLuint sourceDepth = MakeDepthArray();
|
||||
const GLuint destinationColor = MakeColorArray();
|
||||
const GLuint destinationDepth = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceColor, sourceDepth, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.625);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// A destination pre-cleared to something the blit must overwrite, so "the blit did
|
||||
// nothing" and "the blit landed" are different answers.
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationColor, destinationDepth, kDestinationLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.125);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
const float blitted = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(blitted, 0.625f, 1.0f / 4096.0f)
|
||||
<< "depth blitted onto layer " << kDestinationLayer << " reads back as " << blitted
|
||||
<< (std::fabs(blitted - 0.125f) < 1e-3f ? " - the destination kept its own clear" : "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,524 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - gl_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
|
||||
//
|
||||
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
|
||||
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
|
||||
// rectangles and rasterizes only the first. This file is the other half: every case here routes
|
||||
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
|
||||
//
|
||||
// Three claims, one per case:
|
||||
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
|
||||
// geometry-shader invocation per cell, and every cell must hold its own index.
|
||||
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
|
||||
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
|
||||
// This is the claim that fails loudest against a single-viewport backend, because the
|
||||
// geometry is still in the right place while every depth comes back as viewport 0's.
|
||||
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
|
||||
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
|
||||
// case draws the same primitive into the same index twice, once with the test off and once
|
||||
// with it on, and requires the two results to differ in the documented direction.
|
||||
//
|
||||
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
|
||||
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
|
||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||
// it matters.
|
||||
|
||||
#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 int kViewportCount = 16;
|
||||
constexpr int kGridSide = 4; // 4x4 grid of viewports
|
||||
constexpr int kCellSize = 32; // ... each 32x32
|
||||
constexpr int kSurfaceSide = kGridSide * kCellSize;
|
||||
constexpr GLint kUnwritten = -1;
|
||||
|
||||
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
|
||||
// `invocations` runs it once per viewport off a single input point - the same shape
|
||||
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
|
||||
const char* const kVertexSource = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kGridGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = gl_InvocationID;
|
||||
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();
|
||||
}
|
||||
)";
|
||||
|
||||
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
|
||||
// the SAME index twice under two different scissor-enable states.
|
||||
const char* const kSingleGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
uniform int uViewport;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = uViewport;
|
||||
gl_ViewportIndex = uViewport;
|
||||
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 kIntFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out int fragColor;
|
||||
void main() { fragColor = gsIndex; }
|
||||
)";
|
||||
|
||||
// Two quads, one at each end of clip space, so the fragment stage can report the depth
|
||||
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
|
||||
// it reads back the per-viewport minDepth/maxDepth directly.
|
||||
const char* const kDepthGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 8) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kDepthFragmentSource = R"(#version 410 core
|
||||
layout(location = 0) out float fragColor;
|
||||
void main() { fragColor = gl_FragCoord.z; }
|
||||
)";
|
||||
|
||||
class ViewportArrayScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||
"rendering half of KHR-GL43.viewport_array.";
|
||||
}
|
||||
|
||||
GLint maxViewports = 0;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||
|
||||
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
ResetViewportArrayState();
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ResetViewportArrayState();
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every case starts from the same slate: this fixture shares its context with every
|
||||
// other scenario in the process, and a leftover per-index scissor enable is exactly
|
||||
// the kind of state that would make a later case pass or fail for the wrong reason.
|
||||
static void ResetViewportArrayState() {
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glDepthRange(0.0, 1.0);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
|
||||
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
|
||||
// the cell size because the default framebuffer this scenario also renders into is
|
||||
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
|
||||
static void SetupGridViewports(int cellW, int cellH) {
|
||||
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
data[base + 0] = static_cast<GLfloat>(x * cellW);
|
||||
data[base + 1] = static_cast<GLfloat>(y * cellH);
|
||||
data[base + 2] = static_cast<GLfloat>(cellW);
|
||||
data[base + 3] = static_cast<GLfloat>(cellH);
|
||||
}
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, data.data());
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
|
||||
if (vs == 0) return 0;
|
||||
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
|
||||
if (gs == 0) {
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (fs == 0) {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, gs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled) return shader;
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
|
||||
// distinguishable from "index 0 was drawn here".
|
||||
struct IntTarget {
|
||||
GLuint fbo = 0;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
|
||||
// targets the same way (fillTexture), and an upload cannot be confused with a clear
|
||||
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
|
||||
// asserting "this cell must be untouched" cannot afford.
|
||||
static void FillIntTarget(const IntTarget& target, int width, int height) {
|
||||
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
|
||||
}
|
||||
|
||||
static IntTarget MakeIntTarget(int width, int height) {
|
||||
IntTarget target;
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
|
||||
glGenFramebuffers(1, &target.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
FillIntTarget(target, width, height);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void DestroyIntTarget(IntTarget& target) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
|
||||
static std::vector<GLint> ReadInts(int width, int height) {
|
||||
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
|
||||
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
|
||||
// viewport was selected rather than about edge rounding.
|
||||
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
|
||||
const int px = x * kCellSize + kCellSize / 2;
|
||||
const int py = y * kCellSize + kCellSize / 2;
|
||||
return pixels[static_cast<size_t>(py) * stride + px];
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// --- 1. the viewport rectangle -------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const GLint expected = y * kGridSide + x;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
|
||||
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
|
||||
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
|
||||
}
|
||||
}
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
|
||||
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
|
||||
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
|
||||
const int surfaceW = Gl().Width();
|
||||
const int surfaceH = Gl().Height();
|
||||
ASSERT_GE(surfaceW, kGridSide);
|
||||
ASSERT_GE(surfaceH, kGridSide);
|
||||
const int cellW = surfaceW / kGridSide;
|
||||
const int cellH = surfaceH / kGridSide;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
|
||||
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
|
||||
const char* const kColorFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
|
||||
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
|
||||
|
||||
SetupGridViewports(cellW, cellH);
|
||||
glUseProgram(colorProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
|
||||
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const int px = x * cellW + cellW / 2;
|
||||
const int py = y * cellH + cellH / 2;
|
||||
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
|
||||
const int expected = (y * kGridSide + x) * 16;
|
||||
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
|
||||
// cannot confuse two neighbouring indices.
|
||||
EXPECT_LE(std::abs(red - expected), 1)
|
||||
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
|
||||
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
|
||||
<< "only";
|
||||
}
|
||||
}
|
||||
glDeleteProgram(colorProgram);
|
||||
}
|
||||
|
||||
// --- 2. the depth range --------------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
|
||||
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
|
||||
// the far-plane one, so both ends of viewport i's range land in the same column.
|
||||
constexpr int kWidth = kViewportCount;
|
||||
constexpr int kHeight = 2;
|
||||
|
||||
GLuint texture = 0;
|
||||
GLuint fbo = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, clearValue);
|
||||
|
||||
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
|
||||
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
|
||||
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
|
||||
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, viewports.data());
|
||||
glDepthRangeArrayv(0, kViewportCount, ranges.data());
|
||||
|
||||
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
|
||||
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
|
||||
glUseProgram(depthProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
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 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], 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], farDepth, 1.0e-3f)
|
||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||
}
|
||||
|
||||
glDeleteProgram(depthProgram);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// --- 3. the per-index scissor-test enable --------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
|
||||
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
boxes[base + 0] = x * kCellSize;
|
||||
boxes[base + 1] = y * kCellSize;
|
||||
boxes[base + 2] = kCellSize;
|
||||
boxes[base + 3] = kCellSize;
|
||||
}
|
||||
}
|
||||
glScissorArrayv(0, kViewportCount, boxes.data());
|
||||
|
||||
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
|
||||
glUseProgram(singleProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
|
||||
ASSERT_NE(uViewport, -1);
|
||||
|
||||
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
|
||||
constexpr int kProbeX = kProbeIndex % kGridSide;
|
||||
constexpr int kProbeY = kProbeIndex / kGridSide;
|
||||
|
||||
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
|
||||
glUniform1i(uViewport, kProbeIndex);
|
||||
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
|
||||
<< "the scissored index must still paint inside its own box";
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
if (x == kProbeX && y == kProbeY) continue;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
|
||||
<< " and must be untouched";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (b) scissor test DISABLED for the same index, everything else identical: with no
|
||||
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
|
||||
// given the full framebuffer rectangle, and it is exactly where "leave the last
|
||||
// rectangle bound" would show up as a still-clipped quad.
|
||||
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
|
||||
<< "with the scissor test off for index " << kProbeIndex
|
||||
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glDeleteProgram(singleProgram);
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
} // 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;
|
||||
}
|
||||
}
|
||||
@@ -712,10 +725,18 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetViewport(viewport);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetViewport() const {
|
||||
IntVec4 GLContext::GetViewport() const {
|
||||
return m_renderState.GetViewport();
|
||||
}
|
||||
|
||||
void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
m_renderState.SetViewportIndexed(index, viewport);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetViewportIndexed(Uint index) const {
|
||||
return m_renderState.GetViewportIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetLineWidth(Float width) {
|
||||
m_renderState.SetLineWidth(width);
|
||||
}
|
||||
@@ -953,6 +974,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetDepthRange();
|
||||
}
|
||||
|
||||
void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
m_renderState.SetDepthRangeIndexed(index, range);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const {
|
||||
return m_renderState.GetDepthRangeIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetSampleCoverage(Float value, Bool invert) {
|
||||
m_renderState.SetSampleCoverage(value, invert);
|
||||
}
|
||||
@@ -1017,6 +1046,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetScissorBox();
|
||||
}
|
||||
|
||||
void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
m_renderState.SetScissorBoxIndexed(index, box);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const {
|
||||
return m_renderState.GetScissorBoxIndexed(index);
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
||||
m_framebufferState.GenerateNames(number, framebuffers);
|
||||
|
||||
@@ -198,8 +198,10 @@ namespace MobileGL {
|
||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetRenderStateParameters() const;
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports
|
||||
IntVec4 GetViewport() const; // x, y, width, height; viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -260,8 +262,10 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -276,8 +280,10 @@ namespace MobileGL {
|
||||
FrontFaceMode GetFrontFaceMode() const;
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height; rectangle 0
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
// Transform feedback. The fields below are the state of the transform
|
||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||
@@ -407,9 +413,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
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace MobileGL {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Every viewport's scissor-test bit set, i.e. what glEnable(GL_SCISSOR_TEST) writes.
|
||||
constexpr Uint32 kAllViewportsMask =
|
||||
RenderStateParameters::MAX_VIEWPORTS >= 32
|
||||
? ~0u
|
||||
: (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
|
||||
} // namespace
|
||||
|
||||
RenderState::RenderState() {
|
||||
@@ -32,6 +38,15 @@ namespace MobileGL {
|
||||
for (auto& mask : m_parameters.ColorMasks) {
|
||||
mask = BoolVec4(true, true, true, true);
|
||||
}
|
||||
// Every viewport's depth range starts at (0, 1) - GL 4.6 core table 23.4. The
|
||||
// viewport and scissor rectangles legitimately start all-zero here: their spec
|
||||
// initial value is the size of the window the context is first made current to,
|
||||
// which the frontend does not know yet, so an all-zero rectangle means "never
|
||||
// written" and the backends resolve it against the live surface (see
|
||||
// DirectGLES' SyncRenderState and VulkanRenderer's ApplyGLViewportState).
|
||||
for (auto& range : m_parameters.DepthRanges) {
|
||||
range = FloatVec2(0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
Uint RenderState::GetVersion() const {
|
||||
@@ -47,15 +62,47 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// -------------------- Rasterization --------------------
|
||||
// ARB_viewport_array, "Additions to Chapter 2": Viewport(x, y, w, h) is equivalent to
|
||||
// ViewportIndexedf(i, x, y, w, h) for every i in [0, MAX_VIEWPORTS) - it is not a
|
||||
// synonym for "viewport 0".
|
||||
void RenderState::SetViewport(IntVec4 viewport) {
|
||||
if (m_parameters.Viewport == viewport) return;
|
||||
const FloatVec4 asFloat(static_cast<Float>(viewport.x()), static_cast<Float>(viewport.y()),
|
||||
static_cast<Float>(viewport.z()), static_cast<Float>(viewport.w()));
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.Viewports) {
|
||||
if (stored == asFloat) continue;
|
||||
stored = asFloat;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
m_parameters.Viewport = viewport;
|
||||
IntVec4 RenderState::GetViewport() const {
|
||||
const FloatVec4& viewport = m_parameters.Viewports[0];
|
||||
// Round rather than truncate: glGetIntegerv on floating-point state rounds to
|
||||
// nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport to 63 would
|
||||
// also hand the backends a rectangle one pixel short of what was asked for.
|
||||
return IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
||||
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
||||
}
|
||||
|
||||
void RenderState::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.Viewports[index] == viewport) return;
|
||||
|
||||
m_parameters.Viewports[index] = viewport;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetViewport() const {
|
||||
return m_parameters.Viewport;
|
||||
const FloatVec4& RenderState::GetViewportIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_parameters.Viewports[0];
|
||||
}
|
||||
return m_parameters.Viewports[index];
|
||||
}
|
||||
|
||||
void RenderState::SetLineWidth(Float width) {
|
||||
@@ -223,7 +270,6 @@ namespace MobileGL {
|
||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||
SET_CAPABILITY(SampleCoverage, enabled);
|
||||
SET_CAPABILITY(SampleMask, enabled);
|
||||
SET_CAPABILITY(ScissorTest, enabled);
|
||||
SET_CAPABILITY(StencilTest, enabled);
|
||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||
case CapabilityInput::Blend: {
|
||||
@@ -236,6 +282,17 @@ namespace MobileGL {
|
||||
if (stateChanged) BumpVersions();
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core 17.3.2: the non-indexed Enable/Disable(SCISSOR_TEST) enables or
|
||||
// disables the test for ALL viewports, exactly like glViewport writes all
|
||||
// viewports. Anything narrower fails KHR-GL43.viewport_array.scissor_test_state_api,
|
||||
// whose "enable all" phase reads every index back through glIsEnabledi.
|
||||
case CapabilityInput::ScissorTest: {
|
||||
const Uint32 updated = enabled ? kAllViewportsMask : 0u;
|
||||
if (m_parameters.ScissorTestEnabledMask == updated) break;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
break;
|
||||
}
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -287,11 +344,14 @@ namespace MobileGL {
|
||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||
RETURN_CAPABILITY(SampleCoverage);
|
||||
RETURN_CAPABILITY(SampleMask);
|
||||
RETURN_CAPABILITY(ScissorTest);
|
||||
RETURN_CAPABILITY(StencilTest);
|
||||
RETURN_CAPABILITY(ProgramPointSize);
|
||||
case CapabilityInput::Blend:
|
||||
return m_parameters.BlendStates[0].Enabled;
|
||||
// The non-indexed query of an indexed capability answers for index 0
|
||||
// (GL 4.6 core 22.1), which is also the only bit either backend consumes today.
|
||||
case CapabilityInput::ScissorTest:
|
||||
return (m_parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -307,13 +367,29 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
||||
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
||||
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
||||
// exception through the C GL ABI and terminates the process.
|
||||
// GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are
|
||||
// the only indexed capabilities in GL 4.6 core. The GL entry points
|
||||
// (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM
|
||||
// and every out-of-range index with GL_INVALID_VALUE before reaching here, so the
|
||||
// guards below are backstops - but they must stay backstops:
|
||||
// THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and
|
||||
// terminates the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
const Uint32 bit = 1u << index;
|
||||
const Uint32 updated = enabled ? (m_parameters.ScissorTestEnabledMask | bit)
|
||||
: (m_parameters.ScissorTestEnabledMask & ~bit);
|
||||
if (updated == m_parameters.ScissorTestEnabledMask) return;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
return;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
||||
"GL_BLEND (cap=%d, index=%u); ignoring",
|
||||
"GL_BLEND and GL_SCISSOR_TEST (cap=%d, index=%u); ignoring",
|
||||
static_cast<int>(cap), index);
|
||||
return;
|
||||
}
|
||||
@@ -328,9 +404,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
||||
// query must never be able to terminate the process.
|
||||
// GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as
|
||||
// SetCapabilityIndexed: glIsEnabledi has already answered
|
||||
// GL_INVALID_ENUM/GL_INVALID_VALUE for anything else, and a query must never be
|
||||
// able to terminate the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return false;
|
||||
}
|
||||
return (m_parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||
@@ -591,15 +675,39 @@ namespace MobileGL {
|
||||
return m_parameters.BlendColor;
|
||||
}
|
||||
|
||||
// Like Viewport: ARB_viewport_array makes DepthRange(n, f) the same as
|
||||
// DepthRangeIndexed(i, n, f) for every i.
|
||||
void RenderState::SetDepthRange(FloatVec2 range) {
|
||||
if (m_parameters.DepthRange == range) return;
|
||||
|
||||
m_parameters.DepthRange = range;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.DepthRanges) {
|
||||
if (stored == range) continue;
|
||||
stored = range;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRange() const {
|
||||
return m_parameters.DepthRange;
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
|
||||
void RenderState::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.DepthRanges[index] == range) return;
|
||||
|
||||
m_parameters.DepthRanges[index] = range;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRangeIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
return m_parameters.DepthRanges[index];
|
||||
}
|
||||
|
||||
void RenderState::SetSampleCoverage(Float value, Bool invert) {
|
||||
@@ -726,15 +834,39 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// --------------------- Scissor ---------------------
|
||||
// Like Viewport: ARB_viewport_array makes Scissor(x, y, w, h) the same as
|
||||
// ScissorIndexed(i, x, y, w, h) for every i.
|
||||
void RenderState::SetScissorBox(IntVec4 box) {
|
||||
if (m_parameters.ScissorBox == box) return;
|
||||
|
||||
m_parameters.ScissorBox = box;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.ScissorBoxes) {
|
||||
if (stored == box) continue;
|
||||
stored = box;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBox() const {
|
||||
return m_parameters.ScissorBox;
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
|
||||
void RenderState::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.ScissorBoxes[index] == box) return;
|
||||
|
||||
m_parameters.ScissorBoxes[index] = box;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBoxIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
return m_parameters.ScissorBoxes[index];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -220,8 +220,22 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
@@ -247,7 +261,13 @@ namespace MobileGL {
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
FloatVec2 DepthRange = FloatVec2(0.0f, 1.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
@@ -299,10 +319,15 @@ namespace MobileGL {
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool ScissorTestEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
@@ -323,8 +348,14 @@ namespace MobileGL {
|
||||
const RenderStateParameters& GetAllParameters() const;
|
||||
|
||||
// Rasterization
|
||||
// ARB_viewport_array defines glViewport as ViewportIndexedf on EVERY index, so the
|
||||
// classic setter broadcasts; GetViewport answers for index 0 (rounded to the
|
||||
// integers glGetIntegerv(GL_VIEWPORT) and both backends want) and is BY VALUE for
|
||||
// that reason. The indexed pair is the verbatim float state.
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
IntVec4 GetViewport() const; // x, y, width, height, viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -400,8 +431,12 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
// glDepthRange(f) writes every viewport's range (ARB_viewport_array); the indexed
|
||||
// pair is glDepthRangeIndexed / glDepthRangeArrayv. GetDepthRange answers index 0.
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -421,9 +456,12 @@ namespace MobileGL {
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
|
||||
// Scissor
|
||||
// Scissor. glScissor writes every rectangle (ARB_viewport_array); GetScissorBox
|
||||
// answers for index 0.
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
private:
|
||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||
|
||||
@@ -8,9 +8,28 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
|
||||
TEST(DirectVulkanSanity, ProgramMovePreservesViewportIndexUsage) {
|
||||
using VkProgramObject = MobileGL::MG_Backend::DirectVulkan::ProgramFactory::VkProgramObject;
|
||||
|
||||
VkProgramObject moveConstructedSource;
|
||||
moveConstructedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveConstructed(std::move(moveConstructedSource));
|
||||
EXPECT_TRUE(moveConstructed.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveConstructedSource.writesViewportIndexBuiltin);
|
||||
|
||||
VkProgramObject moveAssignedSource;
|
||||
moveAssignedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveAssigned;
|
||||
moveAssigned = std::move(moveAssignedSource);
|
||||
EXPECT_TRUE(moveAssigned.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveAssignedSource.writesViewportIndexBuiltin);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ExtensionEnumeration) {
|
||||
uint32_t extensionCount = 0;
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
||||
|
||||
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
|
||||
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
|
||||
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
|
||||
TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto esWithoutIndirect =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithBoth =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithBoth =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
|
||||
}
|
||||
|
||||
TEST(DrawIndirectCapabilities, RequiresEs31AndBothCoreEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawElementsIndirect = [](GLenum, GLenum, const void*) {};
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsDrawIndirect);
|
||||
|
||||
// The same pointers on an ES 3.0 context are not core entry points and cannot back the
|
||||
// desktop extension contract.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es30Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es30Caps, funcs));
|
||||
EXPECT_FALSE(es30Caps.SupportsDrawIndirect);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto missingElements = MakeFakeGLESFunctions();
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, missingElements));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsDrawIndirect);
|
||||
}
|
||||
|
||||
TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawArraysInstancedBaseInstanceEXT = [](GLenum, GLint, GLsizei, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseVertexBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint) {};
|
||||
|
||||
// Resolved stubs alone must never make the capability true.
|
||||
MobileGL::MG_External::GLESCapabilities pointersOnlyCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(pointersOnlyCaps, funcs));
|
||||
EXPECT_FALSE(pointersOnlyCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT = nullptr;
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@ add_executable(
|
||||
PipelineQuirkTest
|
||||
PipelineQuirkTest.cpp
|
||||
PassthroughTessControlTest.cpp
|
||||
ViewportIndexReflectionTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipelineQuirkTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.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
|
||||
//
|
||||
// ProgramFactory::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
||||
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
||||
// directions and neither direction is caught by a state test:
|
||||
//
|
||||
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
||||
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
||||
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
||||
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
||||
//
|
||||
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
||||
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
||||
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
||||
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
||||
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
||||
//
|
||||
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
||||
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Owns the reflection module so a failing EXPECT cannot leak it.
|
||||
class ReflectModule {
|
||||
public:
|
||||
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) return;
|
||||
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS;
|
||||
}
|
||||
~ReflectModule() {
|
||||
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
||||
}
|
||||
ReflectModule(const ReflectModule&) = delete;
|
||||
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||
|
||||
Bool Created() const { return m_created; }
|
||||
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||
|
||||
private:
|
||||
SpvReflectShaderModule m_module{};
|
||||
Bool m_created = false;
|
||||
};
|
||||
|
||||
class ViewportIndexReflectionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
)";
|
||||
|
||||
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
||||
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
||||
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
||||
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();
|
||||
}
|
||||
)";
|
||||
|
||||
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();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainVertex = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kPlainFragment = R"(#version 410 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
||||
"collapses every index onto viewport 0";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
||||
"single-viewport fast path for nothing";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
||||
// The shape every real application ships: neither stage may widen the pipeline.
|
||||
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
||||
ASSERT_TRUE(vertexModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
||||
|
||||
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
||||
ASSERT_TRUE(fragmentModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
||||
// A default-constructed module has no entry points. The scan runs on every link, so it
|
||||
// must survive a reflection that never got built rather than walk a null array.
|
||||
SpvReflectShaderModule emptyModule{};
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
|
||||
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
}
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
}
|
||||
|
||||
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
|
||||
float fma
|
||||
(float a, float b, float c) { return a * b + c; }
|
||||
float sinh(float x, float y) { return x * y; }
|
||||
float length_squared(vec3 value) { return dot(value, value); }
|
||||
float round(float x) { return floor(x + 0.5); }
|
||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||
|
||||
void main() {
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
|
||||
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
|
||||
}
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
@@ -2707,6 +2709,7 @@ void main() {
|
||||
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||
|
||||
@@ -52,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
OpEntryPoint Fragment %main "main" %outColor
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %globalSampler "sampler"
|
||||
OpName %globalNew "new"
|
||||
OpName %paramSampler "sampler"
|
||||
OpName %paramNew "new"
|
||||
OpName %main "main"
|
||||
OpDecorate %outColor Location 0
|
||||
%void = OpTypeVoid
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%mainFn = OpTypeFunction %void
|
||||
%paramFn = OpTypeFunction %void %float
|
||||
%paramFn = OpTypeFunction %void %float %float
|
||||
%outV4Ptr = OpTypePointer Output %v4float
|
||||
%privatePtr = OpTypePointer Private %float
|
||||
%outColor = OpVariable %outV4Ptr Output
|
||||
%globalSampler = OpVariable %privatePtr Private
|
||||
%globalNew = OpVariable %privatePtr Private
|
||||
%helper = OpFunction %void None %paramFn
|
||||
%paramSampler = OpFunctionParameter %float
|
||||
%paramNew = OpFunctionParameter %float
|
||||
%helperBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
@@ -91,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
|
||||
|
||||
SizeT exactSamplerNameCount = 0;
|
||||
SizeT searchOffset = 0;
|
||||
@@ -99,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
searchOffset += std::strlen("\"sampler\"");
|
||||
}
|
||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
||||
|
||||
SizeT exactNewNameCount = 0;
|
||||
searchOffset = 0;
|
||||
while ((searchOffset = outputText.find("\"new\"", searchOffset)) != String::npos) {
|
||||
++exactNewNameCount;
|
||||
searchOffset += std::strlen("\"new\"");
|
||||
}
|
||||
EXPECT_EQ(exactNewNameCount, 1u);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||
@@ -2060,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.
|
||||
@@ -2435,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:
|
||||
@@ -2836,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) {
|
||||
@@ -2867,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)
|
||||
@@ -2905,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";
|
||||
@@ -2964,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);
|
||||
}
|
||||
}
|
||||
@@ -3044,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";
|
||||
@@ -3072,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";
|
||||
@@ -3172,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);
|
||||
@@ -3213,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);
|
||||
@@ -3257,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);
|
||||
@@ -3300,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";
|
||||
}
|
||||
@@ -3550,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)
|
||||
@@ -3602,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);
|
||||
@@ -3636,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);
|
||||
@@ -3660,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";
|
||||
}
|
||||
|
||||
@@ -3684,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";
|
||||
@@ -3735,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)
|
||||
@@ -3800,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);
|
||||
}
|
||||
@@ -3822,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);
|
||||
@@ -3848,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);
|
||||
}
|
||||
|
||||
@@ -3883,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"
|
||||
@@ -3921,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"
|
||||
@@ -3950,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)
|
||||
@@ -3980,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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -710,6 +711,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;
|
||||
|
||||
@@ -936,6 +968,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
|
||||
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::Uint8;
|
||||
|
||||
VkOffset2D offset{};
|
||||
VkExtent2D copyExtent{};
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 0);
|
||||
EXPECT_EQ(offset.y, 1);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 1);
|
||||
EXPECT_EQ(offset.y, 0);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
// Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are
|
||||
// transposed in opposite directions for 90 and 270 degrees.
|
||||
const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'};
|
||||
const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'};
|
||||
const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
Uint8 result[sizeof(expected)]{};
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
|
||||
std::fill(std::begin(result), std::end(result), 0);
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
||||
@@ -2316,3 +2387,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,6 +3,10 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
SpirvPassTest
|
||||
SpirvPassTest.cpp
|
||||
DeriveNumSubgroupsTest.cpp
|
||||
FixIterationRPBarrierTest.cpp
|
||||
FixIterationRPSubgroupScratchTest.cpp
|
||||
EmulateSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
)
|
||||
|
||||
@@ -154,7 +154,6 @@ class DemoteFloat64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
@@ -175,7 +174,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 +209,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 +240,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 +272,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 +286,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 +306,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 +325,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 +350,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 +361,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 +374,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 +458,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,7 +481,7 @@ 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
|
||||
@@ -502,7 +501,7 @@ namespace {
|
||||
EXPECT_FALSE(input.empty());
|
||||
if (input.empty()) return false;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
return Disassemble(output).find("FAbs") != String::npos;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this
|
||||
// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and,
|
||||
// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and
|
||||
// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists for exactly two
|
||||
// capabilities: GL_BLEND, indexed by draw buffer, and GL_SCISSOR_TEST, indexed by viewport
|
||||
// (ARB_viewport_array). Every other capability must come back as GL_INVALID_ENUM per GL 4.6
|
||||
// sec. 17.3.3 - and, far more importantly, must come back at all: RenderState::SetCapabilityIndexed
|
||||
// and IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
||||
//
|
||||
// The second half of this file is the ARB_viewport_array indexed rectangle state. Every one of
|
||||
// glViewportArrayv/glViewportIndexedf(v)/glScissorArrayv/glScissorIndexed(v)/glDepthRangeArrayv/
|
||||
// glDepthRangeIndexed was a MGLOG_W_ONCE stub that raised no error and stored nothing, and the
|
||||
// indexed getters answered EVERY index with viewport 0's value, so a set/get round trip silently
|
||||
// reported the initial state. The assertions below are deliberately state-shaped rather than
|
||||
// render-shaped: this IS the state machine, and the rendering half (gl_ViewportIndex routing) is
|
||||
// asserted separately in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
@@ -21,6 +30,7 @@
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/RenderState/RenderState.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -50,10 +60,11 @@ namespace {
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) {
|
||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonIndexedCapabilities) {
|
||||
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
||||
// a CTS negative test would hit.
|
||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) {
|
||||
// a CTS negative test would hit. GL_SCISSOR_TEST used to be in this list and is not any more:
|
||||
// ARB_viewport_array makes it the second indexed capability (see the tests below).
|
||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_STENCIL_TEST}) {
|
||||
MG_Impl::GLImpl::Enablei(cap, 0);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
@@ -89,3 +100,462 @@ TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// ARB_viewport_array: indexed viewport / scissor / depth-range state
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
constexpr GLuint kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
|
||||
|
||||
Array<Array<GLfloat, 4>, kMaxViewports> ReadAllViewports() {
|
||||
Array<Array<GLfloat, 4>, kMaxViewports> out{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Array<Array<GLdouble, 2>, kMaxViewports> ReadAllDepthRanges() {
|
||||
Array<Array<GLdouble, 2>, kMaxViewports> out{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, out[i].data());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(RenderStateTest, ScissorTestIsIndexedByViewport) {
|
||||
// The exact shape of KHR-GL43.viewport_array.scissor_test_state_api's toggle loop: one index
|
||||
// is flipped and EVERY index is read back, so a broadcast masquerading as an indexed write
|
||||
// cannot pass.
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint toggled = 0; toggled < kMaxViewports; ++toggled) {
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, toggled);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "index " << toggled;
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), i == toggled ? GL_TRUE : GL_FALSE)
|
||||
<< "enabled index " << toggled << ", read index " << i;
|
||||
}
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, toggled);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, toggled), GL_FALSE);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, NonIndexedScissorTestEnableWritesEveryViewport) {
|
||||
// GL 4.6 core 17.3.2: Enable/Disable(SCISSOR_TEST) is "for all viewports". Reading only
|
||||
// index 0 back would let a broadcast-less implementation through, so every index is checked.
|
||||
MG_Impl::GLImpl::Enable(GL_SCISSOR_TEST);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_TRUE) << "index " << i;
|
||||
}
|
||||
// ... and the non-indexed query answers for viewport 0 (GL 4.6 core 22.1).
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_FALSE) << "index " << i;
|
||||
}
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||
|
||||
// An indexed enable on a NON-zero index must not move the non-indexed answer.
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 3);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ScissorTestEnableRejectsAnOutOfRangeViewportIndex) {
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, kMaxViewports), GL_FALSE);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// MAX_VIEWPORTS - 1 is the last LEGAL index and must stay silent.
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, MaxViewportsMatchesTheIndexedStateWidth) {
|
||||
// The advertised limit and the width of the state arrays are the same number by
|
||||
// construction; a divergence would make some index simultaneously legal to the CTS and
|
||||
// out of range to the setters.
|
||||
GLint maxViewports = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(maxViewports, static_cast<GLint>(kMaxViewports));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16";
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) {
|
||||
Array<GLfloat, kMaxViewports * 4> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
written[i * 4 + 0] = static_cast<GLfloat>(i) + 0.125f;
|
||||
written[i * 4 + 1] = static_cast<GLfloat>(i) + 0.25f;
|
||||
written[i * 4 + 2] = static_cast<GLfloat>(64 + i);
|
||||
written[i * 4 + 3] = static_cast<GLfloat>(32 + i);
|
||||
}
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLfloat asFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, asFloat);
|
||||
// Bit-exact: the fractional origin is the whole point of float viewport state, and the
|
||||
// CTS compares with == (0.125 and 0.25 are exact binary fractions, so this is fair).
|
||||
EXPECT_EQ(asFloat[0], written[i * 4 + 0]) << "index " << i << " must round-trip verbatim";
|
||||
EXPECT_EQ(asFloat[1], written[i * 4 + 1]) << "index " << i;
|
||||
EXPECT_EQ(asFloat[2], written[i * 4 + 2]) << "index " << i;
|
||||
EXPECT_EQ(asFloat[3], written[i * 4 + 3]) << "index " << i;
|
||||
|
||||
GLdouble asDouble[4] = {};
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_VIEWPORT, i, asDouble);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(asDouble[c], static_cast<GLdouble>(written[i * 4 + c])) << "index " << i << " component " << c;
|
||||
}
|
||||
|
||||
// The integer widths round to nearest rather than truncate; the .5+ case is pinned by
|
||||
// ViewportRoundsRatherThanTruncatesForIntegerQueries below.
|
||||
GLint asInt[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, i, asInt);
|
||||
EXPECT_EQ(asInt[2], static_cast<GLint>(64 + i)) << "index " << i;
|
||||
EXPECT_EQ(asInt[3], static_cast<GLint>(32 + i)) << "index " << i;
|
||||
|
||||
GLint64 asInt64[4] = {};
|
||||
MG_Impl::GLImpl::GetInteger64i_v(GL_VIEWPORT, i, asInt64);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(asInt64[c], static_cast<GLint64>(asInt[c])) << "index " << i << " component " << c;
|
||||
}
|
||||
|
||||
GLboolean asBool[4] = {};
|
||||
MG_Impl::GLImpl::GetBooleani_v(GL_VIEWPORT, i, asBool);
|
||||
EXPECT_EQ(asBool[2], GL_TRUE) << "index " << i << ": a non-zero width is GL_TRUE";
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportRoundsRatherThanTruncatesForIntegerQueries) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(2, 0.0f, 0.0f, 255.875f, 63.5f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLint asInt[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 2, asInt);
|
||||
EXPECT_EQ(asInt[2], 256);
|
||||
EXPECT_EQ(asInt[3], 64);
|
||||
|
||||
GLfloat asFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 2, asFloat);
|
||||
EXPECT_EQ(asFloat[2], 255.875f) << "the integer query must not disturb the stored float";
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportIndexedWritesTouchExactlyOneIndex) {
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 8, 8);
|
||||
const auto before = ReadAllViewports();
|
||||
|
||||
for (GLuint target = 0; target < kMaxViewports; ++target) {
|
||||
const GLfloat value[4] = {0.375f, 0.375f, 0.625f, 0.625f};
|
||||
// Alternate the two indexed entry points so both are covered by the isolation claim.
|
||||
if (target % 2 == 0) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(target, value[0], value[1], value[2], value[3]);
|
||||
} else {
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(target, value);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const auto after = ReadAllViewports();
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
if (i == target) {
|
||||
EXPECT_EQ(after[i][0], value[0]) << "index " << i;
|
||||
EXPECT_EQ(after[i][2], value[2]) << "index " << i;
|
||||
} else {
|
||||
EXPECT_EQ(after[i], before[i]) << "write to " << target << " disturbed index " << i;
|
||||
}
|
||||
}
|
||||
MG_Impl::GLImpl::ViewportIndexedf(target, before[target][0], before[target][1], before[target][2],
|
||||
before[target][3]);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ClassicViewportWritesEveryIndexAndIsVisibleThroughIndexZero) {
|
||||
// Both directions of the aliasing. ARB_viewport_array defines glViewport as ViewportIndexedf
|
||||
// on every index, and glGetIntegerv(GL_VIEWPORT) as viewport 0.
|
||||
MG_Impl::GLImpl::ViewportIndexedf(5, 1.0f, 2.0f, 3.0f, 4.0f);
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLfloat data[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, data);
|
||||
EXPECT_EQ(data[0], 0.0f) << "index " << i;
|
||||
EXPECT_EQ(data[2], 1.0f) << "glViewport must overwrite index " << i;
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::ViewportIndexedf(0, 4.0f, 5.0f, 6.0f, 7.0f);
|
||||
GLint classic[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_VIEWPORT, classic);
|
||||
EXPECT_EQ(classic[0], 4);
|
||||
EXPECT_EQ(classic[2], 6);
|
||||
GLfloat classicFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_VIEWPORT, classicFloat);
|
||||
EXPECT_EQ(classicFloat[2], 6.0f);
|
||||
// Index 5 keeps its own value: writing index 0 is not a broadcast.
|
||||
GLfloat other[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 5, other);
|
||||
EXPECT_EQ(other[2], 1.0f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ScissorBoxRoundTripsPerIndexAndAliasesIndexZero) {
|
||||
Array<GLint, kMaxViewports * 4> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
written[i * 4 + 0] = static_cast<GLint>(i);
|
||||
written[i * 4 + 1] = static_cast<GLint>(i * 2);
|
||||
written[i * 4 + 2] = static_cast<GLint>(16 + i);
|
||||
written[i * 4 + 3] = static_cast<GLint>(8 + i);
|
||||
}
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLint readBack[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, readBack);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(readBack[c], written[i * 4 + c]) << "index " << i << " component " << c;
|
||||
}
|
||||
}
|
||||
|
||||
// Indexed writes stay indexed; both spellings.
|
||||
MG_Impl::GLImpl::ScissorIndexed(4, 4, 4, 8, 8);
|
||||
const GLint indexedV[4] = {9, 9, 12, 12};
|
||||
MG_Impl::GLImpl::ScissorIndexedv(7, indexedV);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
GLint probe[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 4, probe);
|
||||
EXPECT_EQ(probe[2], 8);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 7, probe);
|
||||
EXPECT_EQ(probe[2], 12);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 5, probe);
|
||||
EXPECT_EQ(probe[2], static_cast<GLint>(16 + 5)) << "index 5 must be untouched";
|
||||
|
||||
// glScissor writes every rectangle, and glGetIntegerv(GL_SCISSOR_BOX) reports rectangle 0.
|
||||
MG_Impl::GLImpl::Scissor(2, 3, 5, 6);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, probe);
|
||||
EXPECT_EQ(probe[0], 2) << "index " << i;
|
||||
EXPECT_EQ(probe[2], 5) << "index " << i;
|
||||
}
|
||||
GLint classic[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_SCISSOR_BOX, classic);
|
||||
EXPECT_EQ(classic[2], 5);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, DepthRangeRoundTripsPerIndexAndAliasesIndexZero) {
|
||||
Array<GLdouble, kMaxViewports * 2> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
// Exact binary fractions, like the CTS uses: a float-backed store round-trips them.
|
||||
written[i * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
written[i * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const auto readBack = ReadAllDepthRanges();
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(readBack[i][0], written[i * 2 + 0]) << "index " << i;
|
||||
EXPECT_EQ(readBack[i][1], written[i * 2 + 1]) << "index " << i;
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(9, 0.25, 0.75);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
GLdouble probe[2] = {};
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 9, probe);
|
||||
EXPECT_EQ(probe[0], 0.25);
|
||||
EXPECT_EQ(probe[1], 0.75);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 8, probe);
|
||||
EXPECT_EQ(probe[0], 8.0 / 16.0) << "index 8 must be untouched";
|
||||
|
||||
GLfloat asFloat[2] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_DEPTH_RANGE, 9, asFloat);
|
||||
EXPECT_EQ(asFloat[0], 0.25f);
|
||||
EXPECT_EQ(asFloat[1], 0.75f);
|
||||
|
||||
// glDepthRange writes every range; glGetDoublev(GL_DEPTH_RANGE) reports range 0.
|
||||
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, probe);
|
||||
EXPECT_EQ(probe[0], 0.0) << "index " << i;
|
||||
EXPECT_EQ(probe[1], 1.0) << "index " << i;
|
||||
}
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(0, 0.125, 0.875);
|
||||
GLdouble classic[2] = {};
|
||||
MG_Impl::GLImpl::GetDoublev(GL_DEPTH_RANGE, classic);
|
||||
EXPECT_EQ(classic[0], 0.125);
|
||||
EXPECT_EQ(classic[1], 0.875);
|
||||
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, IndexedRectangleSettersRejectAnOutOfRangeIndex) {
|
||||
const GLfloat viewport[4] = {0.0f, 0.0f, 1.0f, 1.0f};
|
||||
const GLint scissor[4] = {0, 0, 1, 1};
|
||||
|
||||
for (const GLuint index : {kMaxViewports, kMaxViewports + 1}) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(index, viewport);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorIndexedv(index, scissor);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(index, 0.0, 1.0);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The last legal index must stay silent - api_errors checks both sides of the boundary.
|
||||
MG_Impl::GLImpl::ViewportIndexedf(kMaxViewports - 1, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ScissorIndexed(kMaxViewports - 1, 0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(kMaxViewports - 1, 0.0, 1.0);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ArraySettersRejectAnOutOfRangeRangeButAcceptAnExactlyFullOne) {
|
||||
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||
Array<GLint, kMaxViewports * 4> scissors{};
|
||||
Array<GLdouble, kMaxViewports * 2> depths{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
viewports[i * 4 + 2] = 1.0f;
|
||||
viewports[i * 4 + 3] = 1.0f;
|
||||
scissors[i * 4 + 2] = 1;
|
||||
scissors[i * 4 + 3] = 1;
|
||||
depths[i * 2 + 1] = 1.0;
|
||||
}
|
||||
|
||||
// first == MAX_VIEWPORTS, and first + count > MAX_VIEWPORTS.
|
||||
MG_Impl::GLImpl::ViewportArrayv(kMaxViewports, 1, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(kMaxViewports, 1, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(kMaxViewports, 1, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// first + count == MAX_VIEWPORTS is LEGAL - the off-by-one an ">=" bound would get wrong,
|
||||
// and one KHR-GL43.viewport_array.api_errors asserts explicitly.
|
||||
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports - 1, viewports.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports - 1, scissors.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports - 1, depths.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// A negative count is GL_INVALID_VALUE and must not be read as a huge unsigned length.
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, -1, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, -1, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(0, -1, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, NegativeExtentsAreRejectedWithoutDisturbingState) {
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, 4, 4);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::Viewport(0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 1, -1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, 1, -1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
for (GLuint index = 0; index < kMaxViewports; ++index) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, -1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, -1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
const GLfloat badW[4] = {0.0f, 0.0f, -1.0f, 1.0f};
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(index, badW);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
const GLint badH[4] = {0, 0, 1, -1};
|
||||
MG_Impl::GLImpl::ScissorIndexedv(index, badH);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// The array form must reject the WHOLE call for one bad element, exactly once, and
|
||||
// leave every rectangle alone - api_errors submits a full 16-element array with a
|
||||
// single negative extent and then requires the error queue to hold one entry.
|
||||
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||
Array<GLint, kMaxViewports * 4> scissors{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
viewports[i * 4 + 2] = 1.0f;
|
||||
viewports[i * 4 + 3] = 1.0f;
|
||||
scissors[i * 4 + 2] = 1;
|
||||
scissors[i * 4 + 3] = 1;
|
||||
}
|
||||
viewports[index * 4 + 2] = -1.0f;
|
||||
scissors[index * 4 + 3] = -1;
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// Nothing above may have landed.
|
||||
GLint viewport[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 0, viewport);
|
||||
EXPECT_EQ(viewport[2], 4);
|
||||
EXPECT_EQ(viewport[3], 4);
|
||||
GLint scissor[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 0, scissor);
|
||||
EXPECT_EQ(scissor[2], 4);
|
||||
EXPECT_EQ(scissor[3], 4);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) {
|
||||
GLint ints[4] = {};
|
||||
GLfloat floats[4] = {};
|
||||
GLdouble doubles[4] = {};
|
||||
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports, ints);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports, floats);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports, doubles);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports - 1, ints);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports - 1, floats);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -4033,3 +4033,306 @@ TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
|
||||
EXPECT_EQ(compressed, GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ===================== glCopyImageSubData validation (KHR-GL43.copy_image) =====================
|
||||
//
|
||||
// Every case below is a mechanism the conformance group caught in the field, and each one is
|
||||
// pinned here because the backend cannot: a wrongly ACCEPTED copy shows up only as wrong pixels
|
||||
// on a device, and a wrongly REJECTED one shows up only as a conformance failure.
|
||||
|
||||
namespace {
|
||||
struct CopyImageSubDataCall {
|
||||
Bool Called = false;
|
||||
GLenum SrcTarget = GL_NONE;
|
||||
GLenum DstTarget = GL_NONE;
|
||||
GLint SrcZ = -1;
|
||||
GLint DstZ = -1;
|
||||
GLsizei Depth = -1;
|
||||
} g_copyImageSubDataCall;
|
||||
|
||||
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
|
||||
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
|
||||
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
|
||||
GLsizei srcHeight, GLsizei srcDepth) {
|
||||
(void)srcTexture;
|
||||
(void)srcLevel;
|
||||
(void)srcX;
|
||||
(void)srcY;
|
||||
(void)dstTexture;
|
||||
(void)dstLevel;
|
||||
(void)dstX;
|
||||
(void)dstY;
|
||||
(void)srcWidth;
|
||||
(void)srcHeight;
|
||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
|
||||
}
|
||||
|
||||
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
||||
// call in every respect except the one the test is about.
|
||||
void MakeCopyImagePair(GLenum srcFormat, GLenum dstFormat, GLuint& srcTexture, GLuint& dstTexture,
|
||||
GLsizei levels = 1, GLsizei extent = 8) {
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, levels, srcFormat, extent, extent);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, levels, dstFormat, extent, extent);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core 18.3.2 compatibility is texel-block SIZE, not base internal format. RGB10_A2 and
|
||||
// R11F_G11F_B10F are both 32-bit and their bases differ (RGBA vs RGB); the old exact-base-format
|
||||
// predicate rejected the pair, which is what took down the whole cross-format half of the
|
||||
// conformance matrix on both backends.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsEqualTexelSizeAcrossDifferentBaseFormats) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGB10_A2, GL_R11F_G11F_B10F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the same rule: equal base format is not sufficient either. RGBA8 and RGBA32F
|
||||
// are both RGBA and 32 vs 128 bits, so the copy is illegal.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsDifferentTexelSizesWithTheSameBaseFormat) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA32F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// ...and the pairing that is legal purely because the sizes agree, across integer-ness too.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsIntegerAndFloatOfTheSameTexelSize) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA32UI, GL_RGBA32F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// 18.3.2 spells a name that is not an object INVALID_VALUE. The shared texture-object validator
|
||||
// says INVALID_OPERATION, which is right for the entry points that reach an object through a
|
||||
// BINDING - hence a rule local to this entry point rather than a change to the helper.
|
||||
TEST_F(TextureTest, CopyImageSubDataNonExistentNameIsInvalidValue) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(4242, GL_TEXTURE_2D, 0, 0, 0, 0, 4243, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// A target that disagrees with the object it names is INVALID_ENUM, not the INVALID_OPERATION the
|
||||
// shared target-uniformity validator records for the upload paths.
|
||||
TEST_F(TextureTest, CopyImageSubDataTargetNotMatchingTheObjectIsInvalidEnum) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY, 0, 0,
|
||||
0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
// The eleven whole-image targets only: a cube FACE converts to a target the frontend knows, so the
|
||||
// generic target validator lets it through, but 18.3.2 does not accept it here.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsTargetsOutsideTheSpecList) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, 0, dstTexture,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
// A level the image does not have is INVALID_VALUE; a single-level texture asked for level 1 used
|
||||
// to reach the backend with whatever the storage layer answered for that level.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsLevelTheImageDoesNotHave) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 1, 0, 0, 0,
|
||||
1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// Sample counts must match. A single-sample image reports zero, so this same comparison is also
|
||||
// what refuses a copy between a multisample target and a non-multisample one.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsSampleCountMismatch) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
// Two DIFFERENT counts are the whole point, so the case needs a context that can actually
|
||||
// create multisample storage - which this unit-test binary, with no backend behind the
|
||||
// renderable-format and sample-count queries, may not be able to. The precondition is
|
||||
// checked on the state objects rather than assumed, so this can only ever skip or test the
|
||||
// real rule; it can never pass vacuously.
|
||||
GLint maxSamples = 1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(srcTexture, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(dstTexture, std::max(maxSamples, 2), GL_RGBA8, 8, 8, GL_FALSE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const Int srcSamples = MG_State::pGLContext->GetTextureObject(srcTexture)->GetSamples();
|
||||
const Int dstSamples = MG_State::pGLContext->GetTextureObject(dstTexture)->GetSamples();
|
||||
if (srcSamples == dstSamples) {
|
||||
GTEST_SKIP() << "this context could not give the two textures different sample counts (both " << srcSamples
|
||||
<< "); nothing for the rule to reject";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstTexture,
|
||||
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// The layer range has to survive the frontend intact. Both backends used to drop it - DirectVulkan
|
||||
// pinned baseArrayLayer/layerCount at 0/1 - so a 12-layer copy moved one layer and said nothing;
|
||||
// this pins the frontend half of that contract.
|
||||
TEST_F(TextureTest, CopyImageSubDataForwardsTheWholeLayerRangeToTheBackend) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||
0, 0, 0, 5, 4, 4, 7);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.SrcZ, 2);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstZ, 5);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.Depth, 7);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The shape KHR-GL43.copy_image.invalid_object ends on once the invalid-name cases are answered
|
||||
// correctly: two ordinary glTexImage2D textures, no storage object, one texel copied from the
|
||||
// origin. Nothing about it is exotic, which is exactly why it is worth a case of its own - every
|
||||
// rule added to this validator is a new way to reject it.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &srcTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, srcTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::GenTextures(1, &dstTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, dstTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
1, 1, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and again after the names have been through a delete/regenerate cycle, which is what the
|
||||
// conformance case does between its sub-cases: it deletes an object to make it invalid, then
|
||||
// builds the next pair from names the allocator hands straight back.
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &srcTexture);
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &dstTexture);
|
||||
DrainPendingGlErrors();
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint reusedSrc = 0;
|
||||
GLuint reusedDst = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
|
||||
1, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A rectangle target reaches the backend as itself. The translation to the GL_TEXTURE_2D the ES
|
||||
// driver actually stores it in belongs to DirectGLES, not here - and putting it here would break
|
||||
// DirectVulkan, which needs the real target to tell an array copy from a flat one.
|
||||
TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstTexture, GL_TEXTURE_RECTANGLE,
|
||||
0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.SrcTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -955,6 +955,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||
caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr &&
|
||||
glesFuncs.glDrawElementsIndirect != nullptr;
|
||||
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
|
||||
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
||||
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
||||
@@ -976,6 +978,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
|
||||
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
|
||||
caps.SupportsDualSourceBlend ? "yes" : "no");
|
||||
MGLOG_I(" draw indirect (ES 3.1 core): %s", caps.SupportsDrawIndirect ? "yes" : "no");
|
||||
MGLOG_I(" multi-draw indirect (EXT_multi_draw_indirect): %s",
|
||||
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
||||
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
||||
@@ -1000,7 +1003,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
||||
GLfloat smoothLineWidthGranularity = 1.0f;
|
||||
GLfloat aliasedPointSizeRange[2] = {1.0f, 1.0f};
|
||||
GLfloat viewportBoundsRange[2] = {0.0f, 0.0f};
|
||||
// GL 4.6 core table 23.60 sets the MINIMUM VIEWPORT_BOUNDS_RANGE at [-32768, 32767], and
|
||||
// KHR-GL43.viewport_array.queries asserts exactly that floor. GLES has no such query, so
|
||||
// the glGetFloatv below raises GL_INVALID_ENUM and leaves this untouched - starting it at
|
||||
// {0, 0} advertised a range that admits no viewport origin at all.
|
||||
GLfloat viewportBoundsRange[2] = {-32768.0f, 32767.0f};
|
||||
GLint maxViewportDims[2] = {16384, 16384};
|
||||
GLint viewportSubpixelBits = 0;
|
||||
GLint max3DTextureSize = 16384;
|
||||
@@ -1289,8 +1296,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxViewports = maxViewports;
|
||||
caps.MaxViewportWidth = maxViewportDims[0];
|
||||
caps.MaxViewportHeight = maxViewportDims[1];
|
||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
||||
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
||||
// exceed the floor but never to sit inside it, and a driver that rejected the query left
|
||||
// the floor in place. Written as a clamp rather than a plain assignment so a partial
|
||||
// write (one component answered, the other not) cannot narrow the range either.
|
||||
caps.ViewportBoundsRangeMin = std::min(viewportBoundsRange[0], -32768.0f);
|
||||
caps.ViewportBoundsRangeMax = std::max(viewportBoundsRange[1], 32767.0f);
|
||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
||||
|
||||
@@ -1149,6 +1149,10 @@ namespace MobileGL {
|
||||
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
||||
// interpolateAtOffset and the three fragment-offset limit queries.
|
||||
Bool SupportsShaderMultisampleInterpolation = false;
|
||||
// ES 3.1+ exposes glDrawArraysIndirect / glDrawElementsIndirect in core. Keep the
|
||||
// version and both entry-point checks together so extension advertisement and the
|
||||
// DirectGLES dispatch path cannot disagree on whether native indirect draws exist.
|
||||
Bool SupportsDrawIndirect = false;
|
||||
// GL_EXT_multi_draw_indirect is present AND glMultiDrawArraysIndirectEXT /
|
||||
// glMultiDrawElementsIndirectEXT both resolved. Multi-draw is not core in any ES
|
||||
// version, and eglGetProcAddress may return a live-looking stub on drivers without
|
||||
|
||||
@@ -33,13 +33,13 @@ namespace MobileGL {
|
||||
|
||||
std::string GetThreadName() {
|
||||
char buffer[64] = {0};
|
||||
#if defined(_WIN32) && !defined(__MINGW32__)
|
||||
#if defined(_WIN32)
|
||||
PWSTR desc = nullptr;
|
||||
if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &desc))) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, desc, -1, buffer, sizeof(buffer), nullptr, nullptr);
|
||||
LocalFree(desc);
|
||||
}
|
||||
#elif defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__) || defined(__MINGW32__)
|
||||
#elif defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__)
|
||||
pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
|
||||
#endif
|
||||
return buffer[0] ? buffer : "UnknownThread";
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "DriverPost.h"
|
||||
#include "DriverPostIterationRPWitness.h"
|
||||
#include "DriverPostIterationRPWitnessSpv.h"
|
||||
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
@@ -24,6 +26,8 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
@@ -1168,7 +1172,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
|
||||
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
|
||||
summary.caps.SupportsDrawIndirect,
|
||||
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
@@ -1452,6 +1458,437 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
disabledNote);
|
||||
}
|
||||
|
||||
// Native iterationRP compute witness. This deliberately uses a separate
|
||||
// throwaway Vulkan device rather than the real renderer's queues, and it
|
||||
// treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
||||
// driver does, not what MobileGL elects to advertise to applications.
|
||||
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 computeQueueFamilyIndex,
|
||||
const VkPhysicalDeviceProperties& properties,
|
||||
Bool subgroupPropertiesAvailable,
|
||||
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
|
||||
constexpr const char* RowName = "Subgroup first-reduction witness";
|
||||
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
|
||||
|
||||
if (!subgroupPropertiesAvailable) {
|
||||
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
|
||||
return;
|
||||
}
|
||||
|
||||
IterationRPWitnessLimits limits{};
|
||||
limits.computeStageSupported =
|
||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
|
||||
limits.basicSubgroupSupported =
|
||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
|
||||
limits.arithmeticSubgroupSupported =
|
||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0;
|
||||
limits.subgroupSize = subgroupProperties.subgroupSize;
|
||||
limits.maxComputeWorkGroupInvocations = properties.limits.maxComputeWorkGroupInvocations;
|
||||
limits.maxComputeWorkGroupSize = {properties.limits.maxComputeWorkGroupSize[0],
|
||||
properties.limits.maxComputeWorkGroupSize[1],
|
||||
properties.limits.maxComputeWorkGroupSize[2]};
|
||||
limits.maxComputeSharedMemorySize = properties.limits.maxComputeSharedMemorySize;
|
||||
limits.maxPerStageDescriptorStorageBuffers = properties.limits.maxPerStageDescriptorStorageBuffers;
|
||||
limits.maxDescriptorSetStorageBuffers = properties.limits.maxDescriptorSetStorageBuffers;
|
||||
limits.maxBoundDescriptorSets = properties.limits.maxBoundDescriptorSets;
|
||||
limits.maxStorageBufferRange = properties.limits.maxStorageBufferRange;
|
||||
|
||||
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
||||
builder.Info(RowName, eligibility.detail);
|
||||
return;
|
||||
}
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
|
||||
fail(eligibility.detail);
|
||||
return;
|
||||
}
|
||||
if (computeQueueFamilyIndex == std::numeric_limits<Uint32>::max()) {
|
||||
fail("no compute queue family is available for the native Vulkan witness");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto vkGetPhysicalDeviceMemoryPropertiesFn =
|
||||
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
getInstanceProcAddr(instance, "vkGetPhysicalDeviceMemoryProperties"));
|
||||
const auto vkCreateDeviceFn =
|
||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||
const auto vkDestroyDeviceFn =
|
||||
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
|
||||
const auto vkGetDeviceQueueFn =
|
||||
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
|
||||
const auto vkCreateBufferFn =
|
||||
reinterpret_cast<PFN_vkCreateBuffer>(getInstanceProcAddr(instance, "vkCreateBuffer"));
|
||||
const auto vkDestroyBufferFn =
|
||||
reinterpret_cast<PFN_vkDestroyBuffer>(getInstanceProcAddr(instance, "vkDestroyBuffer"));
|
||||
const auto vkGetBufferMemoryRequirementsFn = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(
|
||||
getInstanceProcAddr(instance, "vkGetBufferMemoryRequirements"));
|
||||
const auto vkAllocateMemoryFn =
|
||||
reinterpret_cast<PFN_vkAllocateMemory>(getInstanceProcAddr(instance, "vkAllocateMemory"));
|
||||
const auto vkFreeMemoryFn =
|
||||
reinterpret_cast<PFN_vkFreeMemory>(getInstanceProcAddr(instance, "vkFreeMemory"));
|
||||
const auto vkBindBufferMemoryFn =
|
||||
reinterpret_cast<PFN_vkBindBufferMemory>(getInstanceProcAddr(instance, "vkBindBufferMemory"));
|
||||
const auto vkMapMemoryFn =
|
||||
reinterpret_cast<PFN_vkMapMemory>(getInstanceProcAddr(instance, "vkMapMemory"));
|
||||
const auto vkUnmapMemoryFn =
|
||||
reinterpret_cast<PFN_vkUnmapMemory>(getInstanceProcAddr(instance, "vkUnmapMemory"));
|
||||
const auto vkCreateDescriptorSetLayoutFn = reinterpret_cast<PFN_vkCreateDescriptorSetLayout>(
|
||||
getInstanceProcAddr(instance, "vkCreateDescriptorSetLayout"));
|
||||
const auto vkDestroyDescriptorSetLayoutFn = reinterpret_cast<PFN_vkDestroyDescriptorSetLayout>(
|
||||
getInstanceProcAddr(instance, "vkDestroyDescriptorSetLayout"));
|
||||
const auto vkCreateDescriptorPoolFn =
|
||||
reinterpret_cast<PFN_vkCreateDescriptorPool>(getInstanceProcAddr(instance, "vkCreateDescriptorPool"));
|
||||
const auto vkDestroyDescriptorPoolFn = reinterpret_cast<PFN_vkDestroyDescriptorPool>(
|
||||
getInstanceProcAddr(instance, "vkDestroyDescriptorPool"));
|
||||
const auto vkAllocateDescriptorSetsFn = reinterpret_cast<PFN_vkAllocateDescriptorSets>(
|
||||
getInstanceProcAddr(instance, "vkAllocateDescriptorSets"));
|
||||
const auto vkUpdateDescriptorSetsFn =
|
||||
reinterpret_cast<PFN_vkUpdateDescriptorSets>(getInstanceProcAddr(instance, "vkUpdateDescriptorSets"));
|
||||
const auto vkCreateShaderModuleFn =
|
||||
reinterpret_cast<PFN_vkCreateShaderModule>(getInstanceProcAddr(instance, "vkCreateShaderModule"));
|
||||
const auto vkDestroyShaderModuleFn =
|
||||
reinterpret_cast<PFN_vkDestroyShaderModule>(getInstanceProcAddr(instance, "vkDestroyShaderModule"));
|
||||
const auto vkCreatePipelineLayoutFn =
|
||||
reinterpret_cast<PFN_vkCreatePipelineLayout>(getInstanceProcAddr(instance, "vkCreatePipelineLayout"));
|
||||
const auto vkDestroyPipelineLayoutFn = reinterpret_cast<PFN_vkDestroyPipelineLayout>(
|
||||
getInstanceProcAddr(instance, "vkDestroyPipelineLayout"));
|
||||
const auto vkCreateComputePipelinesFn = reinterpret_cast<PFN_vkCreateComputePipelines>(
|
||||
getInstanceProcAddr(instance, "vkCreateComputePipelines"));
|
||||
const auto vkDestroyPipelineFn =
|
||||
reinterpret_cast<PFN_vkDestroyPipeline>(getInstanceProcAddr(instance, "vkDestroyPipeline"));
|
||||
const auto vkCreateCommandPoolFn =
|
||||
reinterpret_cast<PFN_vkCreateCommandPool>(getInstanceProcAddr(instance, "vkCreateCommandPool"));
|
||||
const auto vkDestroyCommandPoolFn =
|
||||
reinterpret_cast<PFN_vkDestroyCommandPool>(getInstanceProcAddr(instance, "vkDestroyCommandPool"));
|
||||
const auto vkAllocateCommandBuffersFn = reinterpret_cast<PFN_vkAllocateCommandBuffers>(
|
||||
getInstanceProcAddr(instance, "vkAllocateCommandBuffers"));
|
||||
const auto vkBeginCommandBufferFn =
|
||||
reinterpret_cast<PFN_vkBeginCommandBuffer>(getInstanceProcAddr(instance, "vkBeginCommandBuffer"));
|
||||
const auto vkEndCommandBufferFn =
|
||||
reinterpret_cast<PFN_vkEndCommandBuffer>(getInstanceProcAddr(instance, "vkEndCommandBuffer"));
|
||||
const auto vkCmdBindPipelineFn =
|
||||
reinterpret_cast<PFN_vkCmdBindPipeline>(getInstanceProcAddr(instance, "vkCmdBindPipeline"));
|
||||
const auto vkCmdBindDescriptorSetsFn = reinterpret_cast<PFN_vkCmdBindDescriptorSets>(
|
||||
getInstanceProcAddr(instance, "vkCmdBindDescriptorSets"));
|
||||
const auto vkCmdDispatchFn =
|
||||
reinterpret_cast<PFN_vkCmdDispatch>(getInstanceProcAddr(instance, "vkCmdDispatch"));
|
||||
const auto vkCmdPipelineBarrierFn =
|
||||
reinterpret_cast<PFN_vkCmdPipelineBarrier>(getInstanceProcAddr(instance, "vkCmdPipelineBarrier"));
|
||||
const auto vkCreateFenceFn =
|
||||
reinterpret_cast<PFN_vkCreateFence>(getInstanceProcAddr(instance, "vkCreateFence"));
|
||||
const auto vkDestroyFenceFn =
|
||||
reinterpret_cast<PFN_vkDestroyFence>(getInstanceProcAddr(instance, "vkDestroyFence"));
|
||||
const auto vkQueueSubmitFn =
|
||||
reinterpret_cast<PFN_vkQueueSubmit>(getInstanceProcAddr(instance, "vkQueueSubmit"));
|
||||
const auto vkWaitForFencesFn =
|
||||
reinterpret_cast<PFN_vkWaitForFences>(getInstanceProcAddr(instance, "vkWaitForFences"));
|
||||
const auto vkDeviceWaitIdleFn =
|
||||
reinterpret_cast<PFN_vkDeviceWaitIdle>(getInstanceProcAddr(instance, "vkDeviceWaitIdle"));
|
||||
|
||||
if (vkGetPhysicalDeviceMemoryPropertiesFn == nullptr || vkCreateDeviceFn == nullptr ||
|
||||
vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr || vkCreateBufferFn == nullptr ||
|
||||
vkDestroyBufferFn == nullptr || vkGetBufferMemoryRequirementsFn == nullptr ||
|
||||
vkAllocateMemoryFn == nullptr || vkFreeMemoryFn == nullptr || vkBindBufferMemoryFn == nullptr ||
|
||||
vkMapMemoryFn == nullptr || vkUnmapMemoryFn == nullptr || vkCreateDescriptorSetLayoutFn == nullptr ||
|
||||
vkDestroyDescriptorSetLayoutFn == nullptr || vkCreateDescriptorPoolFn == nullptr ||
|
||||
vkDestroyDescriptorPoolFn == nullptr || vkAllocateDescriptorSetsFn == nullptr ||
|
||||
vkUpdateDescriptorSetsFn == nullptr || vkCreateShaderModuleFn == nullptr ||
|
||||
vkDestroyShaderModuleFn == nullptr || vkCreatePipelineLayoutFn == nullptr ||
|
||||
vkDestroyPipelineLayoutFn == nullptr || vkCreateComputePipelinesFn == nullptr ||
|
||||
vkDestroyPipelineFn == nullptr || vkCreateCommandPoolFn == nullptr || vkDestroyCommandPoolFn == nullptr ||
|
||||
vkAllocateCommandBuffersFn == nullptr || vkBeginCommandBufferFn == nullptr ||
|
||||
vkEndCommandBufferFn == nullptr || vkCmdBindPipelineFn == nullptr ||
|
||||
vkCmdBindDescriptorSetsFn == nullptr || vkCmdDispatchFn == nullptr ||
|
||||
vkCmdPipelineBarrierFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr ||
|
||||
vkQueueSubmitFn == nullptr || vkWaitForFencesFn == nullptr || vkDeviceWaitIdleFn == nullptr) {
|
||||
fail("vkGetInstanceProcAddr could not resolve the Vulkan entry points required for the witness");
|
||||
return;
|
||||
}
|
||||
|
||||
const Float queuePriority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queueInfo{};
|
||||
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
||||
queueInfo.queueFamilyIndex = computeQueueFamilyIndex;
|
||||
queueInfo.queueCount = 1;
|
||||
queueInfo.pQueuePriorities = &queuePriority;
|
||||
|
||||
VkDeviceCreateInfo deviceInfo{};
|
||||
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
|
||||
if (result != VK_SUCCESS || device == VK_NULL_HANDLE) {
|
||||
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkBuffer outputBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory outputMemory = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkShaderModule shaderModule = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
void* mappedOutput = nullptr;
|
||||
Bool fenceWaitTimedOut = false;
|
||||
const ScopeGuard destroyDeviceObjects([&]() {
|
||||
if (fenceWaitTimedOut) {
|
||||
// Match ProbeVulkanTimerQuery: the command may still execute
|
||||
// after a timeout, so intentionally retain every device-owned
|
||||
// resource rather than risking a forever wait or UAF in the ICD.
|
||||
return;
|
||||
}
|
||||
vkDeviceWaitIdleFn(device);
|
||||
if (fence != VK_NULL_HANDLE) vkDestroyFenceFn(device, fence, nullptr);
|
||||
if (commandPool != VK_NULL_HANDLE) vkDestroyCommandPoolFn(device, commandPool, nullptr);
|
||||
if (pipeline != VK_NULL_HANDLE) vkDestroyPipelineFn(device, pipeline, nullptr);
|
||||
if (pipelineLayout != VK_NULL_HANDLE) vkDestroyPipelineLayoutFn(device, pipelineLayout, nullptr);
|
||||
if (shaderModule != VK_NULL_HANDLE) vkDestroyShaderModuleFn(device, shaderModule, nullptr);
|
||||
if (descriptorPool != VK_NULL_HANDLE) vkDestroyDescriptorPoolFn(device, descriptorPool, nullptr);
|
||||
if (descriptorSetLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyDescriptorSetLayoutFn(device, descriptorSetLayout, nullptr);
|
||||
}
|
||||
if (mappedOutput != nullptr) vkUnmapMemoryFn(device, outputMemory);
|
||||
if (outputBuffer != VK_NULL_HANDLE) vkDestroyBufferFn(device, outputBuffer, nullptr);
|
||||
if (outputMemory != VK_NULL_HANDLE) vkFreeMemoryFn(device, outputMemory, nullptr);
|
||||
vkDestroyDeviceFn(device, nullptr);
|
||||
});
|
||||
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
vkGetDeviceQueueFn(device, computeQueueFamilyIndex, 0, &queue);
|
||||
if (queue == VK_NULL_HANDLE) {
|
||||
fail("vkGetDeviceQueue returned a null compute queue");
|
||||
return;
|
||||
}
|
||||
|
||||
VkBufferCreateInfo bufferInfo{};
|
||||
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||
bufferInfo.size = sizeof(IterationRPWitnessOutput);
|
||||
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
result = vkCreateBufferFn(device, &bufferInfo, nullptr, &outputBuffer);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateBuffer(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkMemoryRequirements memoryRequirements{};
|
||||
vkGetBufferMemoryRequirementsFn(device, outputBuffer, &memoryRequirements);
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryPropertiesFn(physicalDevice, &memoryProperties);
|
||||
Uint32 memoryTypeIndex = std::numeric_limits<Uint32>::max();
|
||||
for (Uint32 index = 0; index < memoryProperties.memoryTypeCount; ++index) {
|
||||
const Bool compatible = (memoryRequirements.memoryTypeBits & (1u << index)) != 0u;
|
||||
const VkMemoryPropertyFlags required = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
if (compatible && (memoryProperties.memoryTypes[index].propertyFlags & required) == required) {
|
||||
memoryTypeIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (memoryTypeIndex == std::numeric_limits<Uint32>::max()) {
|
||||
fail("no host-visible/coherent memory type is compatible with the output SSBO");
|
||||
return;
|
||||
}
|
||||
|
||||
VkMemoryAllocateInfo memoryInfo{};
|
||||
memoryInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||
memoryInfo.allocationSize = memoryRequirements.size;
|
||||
memoryInfo.memoryTypeIndex = memoryTypeIndex;
|
||||
result = vkAllocateMemoryFn(device, &memoryInfo, nullptr, &outputMemory);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkAllocateMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
result = vkBindBufferMemoryFn(device, outputBuffer, outputMemory, 0);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkBindBufferMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
result = vkMapMemoryFn(device, outputMemory, 0, sizeof(IterationRPWitnessOutput), 0, &mappedOutput);
|
||||
if (result != VK_SUCCESS || mappedOutput == nullptr) {
|
||||
fail(format("vkMapMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
std::memset(mappedOutput, 0xa5, sizeof(IterationRPWitnessOutput));
|
||||
|
||||
VkDescriptorSetLayoutBinding outputBinding{};
|
||||
outputBinding.binding = 0;
|
||||
outputBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
outputBinding.descriptorCount = 1;
|
||||
outputBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
VkDescriptorSetLayoutCreateInfo descriptorSetLayoutInfo{};
|
||||
descriptorSetLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
descriptorSetLayoutInfo.bindingCount = 1;
|
||||
descriptorSetLayoutInfo.pBindings = &outputBinding;
|
||||
result = vkCreateDescriptorSetLayoutFn(device, &descriptorSetLayoutInfo, nullptr, &descriptorSetLayout);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateDescriptorSetLayout failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkDescriptorPoolSize poolSize{};
|
||||
poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
poolSize.descriptorCount = 1;
|
||||
VkDescriptorPoolCreateInfo descriptorPoolInfo{};
|
||||
descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
descriptorPoolInfo.maxSets = 1;
|
||||
descriptorPoolInfo.poolSizeCount = 1;
|
||||
descriptorPoolInfo.pPoolSizes = &poolSize;
|
||||
result = vkCreateDescriptorPoolFn(device, &descriptorPoolInfo, nullptr, &descriptorPool);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateDescriptorPool failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
VkDescriptorSetAllocateInfo descriptorSetInfo{};
|
||||
descriptorSetInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
descriptorSetInfo.descriptorPool = descriptorPool;
|
||||
descriptorSetInfo.descriptorSetCount = 1;
|
||||
descriptorSetInfo.pSetLayouts = &descriptorSetLayout;
|
||||
result = vkAllocateDescriptorSetsFn(device, &descriptorSetInfo, &descriptorSet);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkAllocateDescriptorSets failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
VkDescriptorBufferInfo outputDescriptor{};
|
||||
outputDescriptor.buffer = outputBuffer;
|
||||
outputDescriptor.offset = 0;
|
||||
outputDescriptor.range = sizeof(IterationRPWitnessOutput);
|
||||
VkWriteDescriptorSet descriptorWrite{};
|
||||
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
descriptorWrite.dstSet = descriptorSet;
|
||||
descriptorWrite.dstBinding = 0;
|
||||
descriptorWrite.descriptorCount = 1;
|
||||
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
descriptorWrite.pBufferInfo = &outputDescriptor;
|
||||
vkUpdateDescriptorSetsFn(device, 1, &descriptorWrite, 0, nullptr);
|
||||
|
||||
VkShaderModuleCreateInfo shaderModuleInfo{};
|
||||
shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||
shaderModuleInfo.codeSize = sizeof(kDriverPostIterationRPWitnessSpv);
|
||||
shaderModuleInfo.pCode = kDriverPostIterationRPWitnessSpv;
|
||||
result = vkCreateShaderModuleFn(device, &shaderModuleInfo, nullptr, &shaderModule);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateShaderModule failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
|
||||
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
pipelineLayoutInfo.setLayoutCount = 1;
|
||||
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
|
||||
result = vkCreatePipelineLayoutFn(device, &pipelineLayoutInfo, nullptr, &pipelineLayout);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreatePipelineLayout failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo shaderStage{};
|
||||
shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
shaderStage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
shaderStage.module = shaderModule;
|
||||
shaderStage.pName = "main";
|
||||
VkComputePipelineCreateInfo pipelineInfo{};
|
||||
pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
|
||||
pipelineInfo.stage = shaderStage;
|
||||
pipelineInfo.layout = pipelineLayout;
|
||||
result = vkCreateComputePipelinesFn(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateComputePipelines failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkCommandPoolCreateInfo commandPoolInfo{};
|
||||
commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||
commandPoolInfo.queueFamilyIndex = computeQueueFamilyIndex;
|
||||
result = vkCreateCommandPoolFn(device, &commandPoolInfo, nullptr, &commandPool);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
VkCommandBufferAllocateInfo commandBufferInfo{};
|
||||
commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
commandBufferInfo.commandPool = commandPool;
|
||||
commandBufferInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
commandBufferInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffersFn(device, &commandBufferInfo, &commandBuffer);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkCommandBufferBeginInfo commandBufferBeginInfo{};
|
||||
commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
result = vkBeginCommandBufferFn(commandBuffer, &commandBufferBeginInfo);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipelineFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdBindDescriptorSetsFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1,
|
||||
&descriptorSet, 0, nullptr);
|
||||
vkCmdDispatchFn(commandBuffer, 1, 1, 1);
|
||||
VkBufferMemoryBarrier hostReadBarrier{};
|
||||
hostReadBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
|
||||
hostReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
|
||||
hostReadBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
hostReadBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
hostReadBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
hostReadBarrier.buffer = outputBuffer;
|
||||
hostReadBarrier.offset = 0;
|
||||
hostReadBarrier.size = sizeof(IterationRPWitnessOutput);
|
||||
vkCmdPipelineBarrierFn(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0,
|
||||
0, nullptr, 1, &hostReadBarrier, 0, nullptr);
|
||||
result = vkEndCommandBufferFn(commandBuffer);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
VkFenceCreateInfo fenceInfo{};
|
||||
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
VkSubmitInfo submitInfo{};
|
||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &commandBuffer;
|
||||
result = vkQueueSubmitFn(queue, 1, &submitInfo, fence);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr Uint64 FenceTimeoutNs = 5'000'000'000ull;
|
||||
result = vkWaitForFencesFn(device, 1, &fence, VK_TRUE, FenceTimeoutNs);
|
||||
if (result != VK_SUCCESS) {
|
||||
fenceWaitTimedOut = true;
|
||||
fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
|
||||
IterationRPWitnessOutput output{};
|
||||
std::memcpy(&output, mappedOutput, sizeof(output));
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
if (!validation.ok) {
|
||||
fail(validation.detail);
|
||||
return;
|
||||
}
|
||||
builder.Pass(RowName, validation.detail);
|
||||
}
|
||||
|
||||
// Everything the "MobileGL reported ..." rows need from the Vulkan device probe.
|
||||
struct VulkanProbeSummary {
|
||||
Bool devicePropsValid = false;
|
||||
@@ -1461,6 +1898,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Bool shaderSubgroupUsable = false;
|
||||
Bool timerQueriesSupported = false;
|
||||
Bool samplerAnisotropySupported = false;
|
||||
Bool drawIndirectFirstInstanceSupported = false;
|
||||
Bool shaderDrawParametersSupported = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1665,6 +2104,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
||||
Uint32 graphicsQueueFamilyIndex = 0;
|
||||
Uint32 graphicsQueueTimestampValidBits = 0;
|
||||
Uint32 computeQueueFamilyIndex = std::numeric_limits<Uint32>::max();
|
||||
for (VkPhysicalDevice candidate : devices) {
|
||||
Uint32 queueFamilyCount = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr);
|
||||
@@ -1680,6 +2120,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
}
|
||||
}
|
||||
if (physicalDevice != VK_NULL_HANDLE) {
|
||||
for (Uint32 familyIndex = 0; familyIndex < queueFamilyCount; ++familyIndex) {
|
||||
const VkQueueFamilyProperties& family = queueFamilies[familyIndex];
|
||||
if (family.queueCount > 0 && (family.queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) {
|
||||
computeQueueFamilyIndex = familyIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1744,6 +2191,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
VkPhysicalDeviceFeatures features{};
|
||||
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
|
||||
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
|
||||
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
|
||||
if (features.multiDrawIndirect == VK_TRUE) {
|
||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||
} else {
|
||||
@@ -1910,6 +2358,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Warn("shaderDrawParameters",
|
||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
||||
}
|
||||
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
||||
|
||||
Bool provokingVertexLast = false;
|
||||
Bool transformFeedbackPreservesProvokingVertex = false;
|
||||
@@ -2014,13 +2463,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"change every N instances change every one");
|
||||
}
|
||||
|
||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||
Bool subgroupPropertiesAvailable = false;
|
||||
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
|
||||
VkPhysicalDeviceProperties2 properties2{};
|
||||
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
properties2.pNext = &subgroupProperties;
|
||||
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
|
||||
subgroupPropertiesAvailable = true;
|
||||
const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 &&
|
||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
|
||||
@@ -2040,6 +2491,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
|
||||
}
|
||||
|
||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||
properties, subgroupPropertiesAvailable, subgroupProperties);
|
||||
|
||||
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
|
||||
builder.Pass("VK_KHR_draw_indirect_count",
|
||||
"supported (count-buffer indirect draws run as single native "
|
||||
@@ -2108,7 +2562,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
|
||||
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
|
||||
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.comp
|
||||
// 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
|
||||
//
|
||||
// Native Vulkan GLSL 450 witness for iterationRP's first subgroup reduction.
|
||||
// It is intentionally independent of the GL 430 integration scenario. The body
|
||||
// below preserves iterationRP's source reduction; the surrounding diagnostics
|
||||
// only observe its topology and cache handoffs.
|
||||
|
||||
#version 450
|
||||
#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;
|
||||
|
||||
const uint kTopologyNonuniformNumSubgroups = 1u << 0u;
|
||||
const uint kTopologyInvalidNumSubgroups = 1u << 1u;
|
||||
const uint kTopologyInvalidSubgroupId = 1u << 2u;
|
||||
const uint kTopologyInvalidSubgroupLane = 1u << 3u;
|
||||
const uint kWitnessMagic = 0x50323033u;
|
||||
|
||||
layout(std430, set = 0, binding = 0) buffer IterationRPWitnessOutput {
|
||||
uint magic;
|
||||
uint topologyFlags;
|
||||
uint numSubgroups;
|
||||
uint loopLength;
|
||||
uint seenSubgroupMask;
|
||||
|
||||
uvec4 owner511;
|
||||
|
||||
uint lastLaneWriterCount[32];
|
||||
uint indexedInputTotal[32];
|
||||
|
||||
vec2 rawPrefix[32];
|
||||
vec2 scanCache[6][32];
|
||||
vec2 finalAverage;
|
||||
} outWitness;
|
||||
|
||||
// iterationRP's cache stays separate from all diagnostic shared state. In
|
||||
// particular, no instrumentation stores through prefixSumCache except source
|
||||
// writes retained below.
|
||||
shared vec2 prefixSumCache[32];
|
||||
shared uint canonicalNumSubgroups;
|
||||
shared uint topologyFlagsShared;
|
||||
shared uint seenSubgroupMaskShared;
|
||||
shared uint lastLaneWriterCountShared[32];
|
||||
shared uint indexedInputTotalShared[32];
|
||||
|
||||
void main() {
|
||||
const uint localInvocationIndex = gl_LocalInvocationIndex;
|
||||
|
||||
// Host memory is deliberately poisoned before dispatch. Initialize only
|
||||
// shared atomic diagnostic state; owner, average, and magic remain poisoned
|
||||
// until their required post-source-barrier writes below.
|
||||
if (localInvocationIndex == 0u) {
|
||||
canonicalNumSubgroups = 0u;
|
||||
topologyFlagsShared = 0u;
|
||||
seenSubgroupMaskShared = 0u;
|
||||
}
|
||||
if (localInvocationIndex < 32u) {
|
||||
lastLaneWriterCountShared[localInvocationIndex] = 0u;
|
||||
indexedInputTotalShared[localInvocationIndex] = 0u;
|
||||
}
|
||||
memoryBarrierShared();
|
||||
barrier();
|
||||
|
||||
// Invocation zero defines the canonical domain. It is broadcast through
|
||||
// shared memory before every invocation records its own raw observation.
|
||||
if (localInvocationIndex == 0u) {
|
||||
canonicalNumSubgroups = gl_NumSubgroups;
|
||||
outWitness.numSubgroups = gl_NumSubgroups;
|
||||
}
|
||||
barrier();
|
||||
|
||||
const uint canonicalN = canonicalNumSubgroups;
|
||||
if (gl_NumSubgroups != canonicalN)
|
||||
atomicOr(topologyFlagsShared, kTopologyNonuniformNumSubgroups);
|
||||
if (gl_NumSubgroups < 2u || gl_NumSubgroups > 32u)
|
||||
atomicOr(topologyFlagsShared, kTopologyInvalidNumSubgroups);
|
||||
if (gl_SubgroupID >= canonicalN || gl_SubgroupID >= 32u)
|
||||
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupId);
|
||||
if (gl_SubgroupSize == 0u || gl_SubgroupInvocationID >= gl_SubgroupSize)
|
||||
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupLane);
|
||||
|
||||
// Keep all atomic collection bounded by the canonical valid domain. A
|
||||
// nonuniform/broken report reaches the uniform safety branch below instead
|
||||
// of making some lanes return before a barrier.
|
||||
const bool canonicalDomain = canonicalN >= 2u && canonicalN <= 32u;
|
||||
const bool idInCanonicalDomain = canonicalDomain && gl_SubgroupID < canonicalN;
|
||||
if (idInCanonicalDomain) {
|
||||
atomicOr(seenSubgroupMaskShared, 1u << gl_SubgroupID);
|
||||
atomicAdd(indexedInputTotalShared[gl_SubgroupID], localInvocationIndex + 1u);
|
||||
if (gl_SubgroupSize != 0u && gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
atomicAdd(lastLaneWriterCountShared[gl_SubgroupID], 1u);
|
||||
}
|
||||
memoryBarrierShared();
|
||||
barrier();
|
||||
|
||||
if (localInvocationIndex == 0u)
|
||||
outWitness.seenSubgroupMask = seenSubgroupMaskShared;
|
||||
if (localInvocationIndex < 32u) {
|
||||
outWitness.lastLaneWriterCount[localInvocationIndex] = lastLaneWriterCountShared[localInvocationIndex];
|
||||
outWitness.indexedInputTotal[localInvocationIndex] = indexedInputTotalShared[localInvocationIndex];
|
||||
}
|
||||
|
||||
// This branch is uniform after collection and is solely a safety guard for
|
||||
// broken topology reports. The valid side retains iterationRP verbatim.
|
||||
const bool sourceDomain = canonicalDomain && topologyFlagsShared == 0u;
|
||||
if (sourceDomain) {
|
||||
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex + 1u), 0.0);
|
||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
||||
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
||||
barrier();
|
||||
|
||||
if (gl_LocalInvocationIndex < gl_NumSubgroups)
|
||||
outWitness.rawPrefix[gl_LocalInvocationIndex] = prefixSumCache[gl_LocalInvocationIndex];
|
||||
barrier();
|
||||
|
||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||
if (gl_LocalInvocationIndex == 0u)
|
||||
outWitness.loopLength = loopLength;
|
||||
|
||||
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 < gl_NumSubgroups)
|
||||
outWitness.scanCache[scanStage][gl_LocalInvocationIndex] =
|
||||
prefixSumCache[gl_LocalInvocationIndex];
|
||||
// A second, diagnostic-only barrier prevents a faster invocation
|
||||
// from entering the next source stage while another reads this cache.
|
||||
barrier();
|
||||
}
|
||||
|
||||
if (gl_LocalInvocationIndex == 511u)
|
||||
prefixSumCache[0] = sampleLuminance / 512.0;
|
||||
barrier();
|
||||
|
||||
if (gl_LocalInvocationIndex == 511u) {
|
||||
outWitness.owner511 = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
|
||||
gl_SubgroupInvocationID);
|
||||
outWitness.finalAverage = prefixSumCache[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Both sides of the uniform branch reach this barrier. The magic is the
|
||||
// completion latch and therefore cannot be written before the final barrier.
|
||||
barrier();
|
||||
if (localInvocationIndex == 0u) {
|
||||
outWitness.topologyFlags = topologyFlagsShared;
|
||||
outWitness.magic = kWitnessMagic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.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 "DriverPostIterationRPWitness.h"
|
||||
|
||||
#include <bit>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
[[nodiscard]] IterationRPWitnessValidationResult Failure(IterationRPWitnessValidationFailure failure,
|
||||
std::string detail,
|
||||
std::uint32_t scanStage = 0u,
|
||||
std::uint32_t subgroup = 0u) {
|
||||
IterationRPWitnessValidationResult result;
|
||||
result.ok = false;
|
||||
result.failure = failure;
|
||||
result.scanStage = scanStage;
|
||||
result.subgroup = subgroup;
|
||||
result.detail = std::move(detail);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t FloatBits(float value) {
|
||||
return std::bit_cast<std::uint32_t>(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SameBits(float lhs, float rhs) {
|
||||
return FloatBits(lhs) == FloatBits(rhs);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SameBits(const IterationRPWitnessVec2& lhs, const IterationRPWitnessVec2& rhs) {
|
||||
return SameBits(lhs.x, rhs.x) && SameBits(lhs.y, rhs.y);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string Vec2String(const IterationRPWitnessVec2& value) {
|
||||
std::ostringstream output;
|
||||
output << '(' << value.x << ',' << value.y << ')';
|
||||
return output.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t ExpectedSeenSubgroupMask(std::uint32_t numSubgroups) {
|
||||
return numSubgroups == kIterationRPWitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string JoinRequirements(const std::vector<std::string>& requirements) {
|
||||
std::ostringstream output;
|
||||
for (std::size_t i = 0; i < requirements.size(); ++i) {
|
||||
if (i != 0u) output << "; ";
|
||||
output << requirements[i];
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
IterationRPWitnessEligibilityResult
|
||||
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits) {
|
||||
// This classification deliberately precedes numeric limits. An absent native
|
||||
// compute/basic/arithmetic subgroup contract means there is nothing to witness,
|
||||
// whereas every resource/entry-point failure on a capable device is a POST FAIL.
|
||||
if (!limits.computeStageSupported || !limits.basicSubgroupSupported || !limits.arithmeticSubgroupSupported) {
|
||||
std::vector<std::string> missing;
|
||||
if (!limits.computeStageSupported) missing.emplace_back("VK_SHADER_STAGE_COMPUTE_BIT");
|
||||
if (!limits.basicSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_BASIC_BIT");
|
||||
if (!limits.arithmeticSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT");
|
||||
return {IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet,
|
||||
"skipped because the native compute/basic/arithmetic subgroup feature set is unsupported (missing " +
|
||||
JoinRequirements(missing) + ')'};
|
||||
}
|
||||
|
||||
std::vector<std::string> inadequate;
|
||||
if (limits.subgroupSize == 0u) {
|
||||
inadequate.emplace_back("subgroupSize == 0");
|
||||
}
|
||||
if (limits.maxComputeWorkGroupInvocations < kIterationRPWitnessInvocationCount) {
|
||||
inadequate.emplace_back("maxComputeWorkGroupInvocations < 512");
|
||||
}
|
||||
if (limits.maxComputeWorkGroupSize[0] < 32u || limits.maxComputeWorkGroupSize[1] < 16u ||
|
||||
limits.maxComputeWorkGroupSize[2] < 1u) {
|
||||
inadequate.emplace_back("maxComputeWorkGroupSize does not cover 32x16x1");
|
||||
}
|
||||
if (limits.maxComputeSharedMemorySize < kIterationRPWitnessSharedMemoryBytes) {
|
||||
inadequate.emplace_back("maxComputeSharedMemorySize < " +
|
||||
std::to_string(kIterationRPWitnessSharedMemoryBytes));
|
||||
}
|
||||
if (limits.maxPerStageDescriptorStorageBuffers < 1u) {
|
||||
inadequate.emplace_back("maxPerStageDescriptorStorageBuffers < 1");
|
||||
}
|
||||
if (limits.maxDescriptorSetStorageBuffers < 1u) {
|
||||
inadequate.emplace_back("maxDescriptorSetStorageBuffers < 1");
|
||||
}
|
||||
if (limits.maxBoundDescriptorSets < 1u) {
|
||||
inadequate.emplace_back("maxBoundDescriptorSets < 1");
|
||||
}
|
||||
if (limits.maxStorageBufferRange < sizeof(IterationRPWitnessOutput)) {
|
||||
inadequate.emplace_back("maxStorageBufferRange < " +
|
||||
std::to_string(sizeof(IterationRPWitnessOutput)));
|
||||
}
|
||||
if (!inadequate.empty()) {
|
||||
return {IterationRPWitnessEligibility::FailInadequateLimits,
|
||||
"insufficient Vulkan limits for a 32x16x1 workgroup, one output SSBO, and " +
|
||||
std::to_string(kIterationRPWitnessSharedMemoryBytes) + " bytes of shared memory: " +
|
||||
JoinRequirements(inadequate)};
|
||||
}
|
||||
return {IterationRPWitnessEligibility::Execute, {}};
|
||||
}
|
||||
|
||||
std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups) {
|
||||
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) return 0u;
|
||||
|
||||
// Exact C++ spelling of the source's findMSB-based calculation. In
|
||||
// particular, its final iteration for powers of two is intentional.
|
||||
std::uint32_t loopLength = 0u;
|
||||
for (std::uint32_t value = numSubgroups; value > 1u; value >>= 1u) {
|
||||
++loopLength;
|
||||
}
|
||||
loopLength += static_cast<std::uint32_t>(numSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||
return loopLength;
|
||||
}
|
||||
|
||||
IterationRPWitnessValidationResult ValidateIterationRPWitness(const IterationRPWitnessOutput& output) {
|
||||
// 1. Completion. A poisoned or unwritten result must never turn into a
|
||||
// topology diagnosis, because it says nothing about execution.
|
||||
if (output.magic != kIterationRPWitnessMagic) {
|
||||
std::ostringstream detail;
|
||||
detail << "completion: magic was 0x" << std::hex << output.magic << ", expected 0x"
|
||||
<< kIterationRPWitnessMagic;
|
||||
return Failure(IterationRPWitnessValidationFailure::Completion, detail.str());
|
||||
}
|
||||
|
||||
// 2. Observed topology. All checks consume observations written by the
|
||||
// shader, rather than inferring subgroup layout from invocation indices.
|
||||
const std::uint32_t numSubgroups = output.numSubgroups;
|
||||
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: canonical gl_NumSubgroups=" << numSubgroups << " is outside [2, 32]";
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
if ((output.topologyFlags & IterationRPWitnessNonuniformNumSubgroups) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: gl_NumSubgroups differed across workgroup");
|
||||
}
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidNumSubgroups) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported gl_NumSubgroups outside [2, 32]");
|
||||
}
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupId) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported an invalid gl_SubgroupID");
|
||||
}
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupLane) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported an invalid subgroup lane");
|
||||
}
|
||||
if ((output.topologyFlags & ~(IterationRPWitnessNonuniformNumSubgroups |
|
||||
IterationRPWitnessInvalidNumSubgroups |
|
||||
IterationRPWitnessInvalidSubgroupId |
|
||||
IterationRPWitnessInvalidSubgroupLane)) != 0u) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: unknown topology flags 0x" << std::hex << output.topologyFlags;
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
const std::uint32_t expectedMask = ExpectedSeenSubgroupMask(numSubgroups);
|
||||
if (output.seenSubgroupMask != expectedMask) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: seen subgroup-ID mask was 0x" << std::hex << output.seenSubgroupMask
|
||||
<< ", expected 0x" << expectedMask;
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
const std::uint32_t expectedLoopLength = ComputeIterationRPWitnessLoopLength(numSubgroups);
|
||||
if (output.loopLength != expectedLoopLength) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: loopLength was " << std::dec << output.loopLength << ", expected "
|
||||
<< expectedLoopLength;
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
if (output.lastLaneWriterCount[subgroup] != 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: subgroup " << subgroup << " has "
|
||||
<< output.lastLaneWriterCount[subgroup] << " source last-lane writers, expected exactly 1";
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str(), 0u, subgroup);
|
||||
}
|
||||
}
|
||||
if (output.owner511.y != numSubgroups) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 reported gl_NumSubgroups=" << output.owner511.y << ", expected "
|
||||
<< numSubgroups;
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
if (output.owner511.z != numSubgroups - 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 is not in the highest subgroup (id" << output.owner511.z
|
||||
<< ", expected id" << (numSubgroups - 1u) << ')';
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
if (output.owner511.x == 0u || output.owner511.w != output.owner511.x - 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 is not the last lane of highest subgroup (size "
|
||||
<< output.owner511.x << ", lane " << output.owner511.w << ')';
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
|
||||
// 3. Initial subgroup handoff. The atomic scalar totals are independent
|
||||
// of subgroupInclusiveAdd; their sum and the cache values establish that
|
||||
// the final lanes handed off the native vector inclusive-add results.
|
||||
std::uint64_t indexedTotal = 0u;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
indexedTotal += output.indexedInputTotal[subgroup];
|
||||
}
|
||||
if (indexedTotal != 131328u) {
|
||||
std::ostringstream detail;
|
||||
detail << "initial subgroup handoff: indexed input total was " << indexedTotal << ", expected 131328";
|
||||
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str());
|
||||
}
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
const IterationRPWitnessVec2 expected = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
|
||||
if (!SameBits(output.rawPrefix[subgroup], expected)) {
|
||||
std::ostringstream detail;
|
||||
detail << "initial subgroup handoff: subgroup " << subgroup << " rawPrefix observed "
|
||||
<< Vec2String(output.rawPrefix[subgroup]) << ", expected " << Vec2String(expected);
|
||||
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u,
|
||||
subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Source scan. Do not substitute a conventional scan: this reproduces
|
||||
// the source cache index expression and stage ordering word for word.
|
||||
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> expectedCache = output.rawPrefix;
|
||||
for (std::uint32_t scanStage = 0u; scanStage < expectedLoopLength; ++scanStage) {
|
||||
auto cacheAfterStage = expectedCache;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
if ((subgroup & (1u << scanStage)) > 0u) {
|
||||
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
|
||||
cacheAfterStage[subgroup].x += expectedCache[sourceCacheIndex].x;
|
||||
cacheAfterStage[subgroup].y += expectedCache[sourceCacheIndex].y;
|
||||
}
|
||||
}
|
||||
expectedCache = cacheAfterStage;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
if (!SameBits(output.scanCache[scanStage][subgroup], expectedCache[subgroup])) {
|
||||
std::ostringstream detail;
|
||||
detail << "source scan stage " << scanStage << ", subgroup " << subgroup << ": observed "
|
||||
<< Vec2String(output.scanCache[scanStage][subgroup]) << ", expected "
|
||||
<< Vec2String(expectedCache[subgroup]);
|
||||
return Failure(IterationRPWitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. The owner contract was checked above with the other topology facts;
|
||||
// this final result remains a separate exact-vector check.
|
||||
const IterationRPWitnessVec2 expectedAverage = {256.5f, 0.0f};
|
||||
if (!SameBits(output.finalAverage, expectedAverage)) {
|
||||
std::ostringstream detail;
|
||||
detail << "final average: observed " << Vec2String(output.finalAverage) << ", expected "
|
||||
<< Vec2String(expectedAverage);
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalAverage, detail.str());
|
||||
}
|
||||
|
||||
std::ostringstream detail;
|
||||
detail << "N=" << numSubgroups << ", owner511=id" << output.owner511.z << "/lane" << output.owner511.w
|
||||
<< ", " << expectedLoopLength << " scan stages, average=" << Vec2String(output.finalAverage);
|
||||
IterationRPWitnessValidationResult result;
|
||||
result.ok = true;
|
||||
result.failure = IterationRPWitnessValidationFailure::None;
|
||||
result.detail = detail.str();
|
||||
return result;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,153 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.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
|
||||
//
|
||||
// Compact, native-Vulkan iterationRP first-reduction witness ABI and its pure
|
||||
// validator. The types below deliberately mirror DriverPostIterationRPWitness.comp's
|
||||
// single std430 storage block; changing either side requires updating the static
|
||||
// layout assertions here.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
// "P203": the pack's trace program id, kept stable so the checked-in witness
|
||||
// SPIR-V (DriverPostIterationRPWitnessSpv.h) needs no regeneration.
|
||||
constexpr std::uint32_t kIterationRPWitnessMagic = 0x50323033u;
|
||||
constexpr std::uint32_t kIterationRPWitnessInvocationCount = 512u;
|
||||
constexpr std::uint32_t kIterationRPWitnessMaxSubgroups = 32u;
|
||||
constexpr std::uint32_t kIterationRPWitnessMaxScanStages = 6u;
|
||||
|
||||
// These bit values are shared with the GLSL source. They document failures in
|
||||
// topology observations rather than guessing a topology from local IDs on the host.
|
||||
enum IterationRPWitnessTopologyFlag : std::uint32_t {
|
||||
IterationRPWitnessNonuniformNumSubgroups = 1u << 0u,
|
||||
IterationRPWitnessInvalidNumSubgroups = 1u << 1u,
|
||||
IterationRPWitnessInvalidSubgroupId = 1u << 2u,
|
||||
IterationRPWitnessInvalidSubgroupLane = 1u << 3u,
|
||||
};
|
||||
|
||||
struct alignas(8) IterationRPWitnessVec2 {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct alignas(16) IterationRPWitnessUVec4 {
|
||||
std::uint32_t x;
|
||||
std::uint32_t y;
|
||||
std::uint32_t z;
|
||||
std::uint32_t w;
|
||||
};
|
||||
|
||||
// std430 layout of DriverPostIterationRPWitness.comp's IterationRPWitnessOutput block.
|
||||
struct alignas(16) IterationRPWitnessOutput {
|
||||
std::uint32_t magic;
|
||||
std::uint32_t topologyFlags;
|
||||
std::uint32_t numSubgroups;
|
||||
std::uint32_t loopLength;
|
||||
std::uint32_t seenSubgroupMask;
|
||||
|
||||
IterationRPWitnessUVec4 owner511;
|
||||
|
||||
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> lastLaneWriterCount;
|
||||
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> indexedInputTotal;
|
||||
|
||||
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> rawPrefix;
|
||||
std::array<std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups>,
|
||||
kIterationRPWitnessMaxScanStages>
|
||||
scanCache;
|
||||
IterationRPWitnessVec2 finalAverage;
|
||||
};
|
||||
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessVec2>);
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessUVec4>);
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessOutput>);
|
||||
static_assert(sizeof(IterationRPWitnessVec2) == 8u);
|
||||
static_assert(alignof(IterationRPWitnessVec2) == 8u);
|
||||
static_assert(sizeof(IterationRPWitnessUVec4) == 16u);
|
||||
static_assert(alignof(IterationRPWitnessUVec4) == 16u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, magic) == 0u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, topologyFlags) == 4u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, numSubgroups) == 8u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, loopLength) == 12u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, seenSubgroupMask) == 16u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, owner511) == 32u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, lastLaneWriterCount) == 48u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, indexedInputTotal) == 176u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, rawPrefix) == 304u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, scanCache) == 560u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, finalAverage) == 2096u);
|
||||
static_assert(sizeof(IterationRPWitnessOutput) == 2112u);
|
||||
|
||||
// The witness uses prefixSumCache[32], three scalar shared diagnostics, and
|
||||
// two 32-entry scalar diagnostic arrays in the GLSL source. Keep this
|
||||
// independent of the output SSBO size.
|
||||
constexpr std::uint32_t kIterationRPWitnessSharedMemoryBytes =
|
||||
kIterationRPWitnessMaxSubgroups * sizeof(IterationRPWitnessVec2) +
|
||||
3u * sizeof(std::uint32_t) +
|
||||
2u * kIterationRPWitnessMaxSubgroups * sizeof(std::uint32_t);
|
||||
|
||||
enum class IterationRPWitnessEligibility {
|
||||
Execute,
|
||||
SkipUnsupportedNativeFeatureSet,
|
||||
FailInadequateLimits,
|
||||
};
|
||||
|
||||
// The raw physical-device conditions needed by the native witness. This is
|
||||
// intentionally distinct from MobileGL's advertised-extension policy.
|
||||
struct IterationRPWitnessLimits {
|
||||
bool computeStageSupported = false;
|
||||
bool basicSubgroupSupported = false;
|
||||
bool arithmeticSubgroupSupported = false;
|
||||
std::uint32_t subgroupSize = 0u;
|
||||
|
||||
std::uint32_t maxComputeWorkGroupInvocations = 0u;
|
||||
std::array<std::uint32_t, 3> maxComputeWorkGroupSize{};
|
||||
std::uint32_t maxComputeSharedMemorySize = 0u;
|
||||
std::uint32_t maxPerStageDescriptorStorageBuffers = 0u;
|
||||
std::uint32_t maxDescriptorSetStorageBuffers = 0u;
|
||||
std::uint32_t maxBoundDescriptorSets = 0u;
|
||||
std::uint64_t maxStorageBufferRange = 0u;
|
||||
};
|
||||
|
||||
struct IterationRPWitnessEligibilityResult {
|
||||
IterationRPWitnessEligibility eligibility = IterationRPWitnessEligibility::FailInadequateLimits;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
enum class IterationRPWitnessValidationFailure {
|
||||
None,
|
||||
Completion,
|
||||
Topology,
|
||||
InitialSubgroupHandoff,
|
||||
SourceScan,
|
||||
FinalOwner,
|
||||
FinalAverage,
|
||||
};
|
||||
|
||||
struct IterationRPWitnessValidationResult {
|
||||
bool ok = false;
|
||||
IterationRPWitnessValidationFailure failure = IterationRPWitnessValidationFailure::Completion;
|
||||
std::uint32_t scanStage = 0u;
|
||||
std::uint32_t subgroup = 0u;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
[[nodiscard]] IterationRPWitnessEligibilityResult
|
||||
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits);
|
||||
|
||||
// Mirrors the source's findMSB expression for valid N in [2, 32].
|
||||
[[nodiscard]] std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups);
|
||||
|
||||
[[nodiscard]] IterationRPWitnessValidationResult
|
||||
ValidateIterationRPWitness(const IterationRPWitnessOutput& output);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,297 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.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
|
||||
//
|
||||
// Generated from DriverPostIterationRPWitness.comp with:
|
||||
// glslangValidator --target-env vulkan1.1 -V DriverPostIterationRPWitness.comp
|
||||
// Validated with spirv-val --target-env vulkan1.1. Do not edit words by hand.
|
||||
//
|
||||
// The stored words predate the Program203 -> IterationRP source rename, so their
|
||||
// embedded OpName debug strings still spell the old identifiers; regeneration from
|
||||
// the renamed source produces semantically identical code differing only in those
|
||||
// strings. The witness magic stays 0x50323033 ("P203" - the trace's program id) so
|
||||
// these words remain valid without regeneration.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
inline constexpr std::uint32_t kDriverPostIterationRPWitnessSpv[] = {
|
||||
0x07230203u, 0x00010300u, 0x0008000bu, 0x00000145u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
|
||||
0x0000003du, 0x00020011u, 0x0000003fu, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x000a000fu, 0x00000005u, 0x00000004u, 0x6e69616du,
|
||||
0x00000000u, 0x0000000au, 0x0000002au, 0x00000050u, 0x0000005eu, 0x00000064u, 0x00060010u, 0x00000004u,
|
||||
0x00000011u, 0x00000020u, 0x00000010u, 0x00000001u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x000a0004u,
|
||||
0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x615f7075u, 0x68746972u, 0x6974656du,
|
||||
0x00000063u, 0x00090004u, 0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x625f7075u,
|
||||
0x63697361u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00080005u, 0x00000008u,
|
||||
0x61636f6cu, 0x766e496cu, 0x7461636fu, 0x496e6f69u, 0x7865646eu, 0x00000000u, 0x00080005u, 0x0000000au,
|
||||
0x4c5f6c67u, 0x6c61636fu, 0x6f766e49u, 0x69746163u, 0x6e496e6fu, 0x00786564u, 0x00080005u, 0x00000013u,
|
||||
0x6f6e6163u, 0x6163696eu, 0x6d754e6cu, 0x67627553u, 0x70756f72u, 0x00000073u, 0x00070005u, 0x00000014u,
|
||||
0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x61685373u, 0x00646572u, 0x00080005u, 0x00000015u, 0x6e656573u,
|
||||
0x67627553u, 0x70756f72u, 0x6b73614du, 0x72616853u, 0x00006465u, 0x00090005u, 0x0000001du, 0x7473616cu,
|
||||
0x656e614cu, 0x74697257u, 0x6f437265u, 0x53746e75u, 0x65726168u, 0x00000064u, 0x00080005u, 0x00000020u,
|
||||
0x65646e69u, 0x49646578u, 0x7475706eu, 0x61746f54u, 0x6168536cu, 0x00646572u, 0x00060005u, 0x0000002au,
|
||||
0x4e5f6c67u, 0x75536d75u, 0x6f726762u, 0x00737075u, 0x00080005u, 0x00000035u, 0x676f7250u, 0x326d6172u,
|
||||
0x69573330u, 0x73656e74u, 0x74754f73u, 0x00747570u, 0x00050006u, 0x00000035u, 0x00000000u, 0x6967616du,
|
||||
0x00000063u, 0x00070006u, 0x00000035u, 0x00000001u, 0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x00000073u,
|
||||
0x00070006u, 0x00000035u, 0x00000002u, 0x536d756eu, 0x72676275u, 0x7370756fu, 0x00000000u, 0x00060006u,
|
||||
0x00000035u, 0x00000003u, 0x706f6f6cu, 0x676e654cu, 0x00006874u, 0x00080006u, 0x00000035u, 0x00000004u,
|
||||
0x6e656573u, 0x67627553u, 0x70756f72u, 0x6b73614du, 0x00000000u, 0x00060006u, 0x00000035u, 0x00000005u,
|
||||
0x656e776fu, 0x31313572u, 0x00000000u, 0x00080006u, 0x00000035u, 0x00000006u, 0x7473616cu, 0x656e614cu,
|
||||
0x74697257u, 0x6f437265u, 0x00746e75u, 0x00080006u, 0x00000035u, 0x00000007u, 0x65646e69u, 0x49646578u,
|
||||
0x7475706eu, 0x61746f54u, 0x0000006cu, 0x00060006u, 0x00000035u, 0x00000008u, 0x50776172u, 0x69666572u,
|
||||
0x00000078u, 0x00060006u, 0x00000035u, 0x00000009u, 0x6e616373u, 0x68636143u, 0x00000065u, 0x00070006u,
|
||||
0x00000035u, 0x0000000au, 0x616e6966u, 0x6576416cu, 0x65676172u, 0x00000000u, 0x00050005u, 0x00000037u,
|
||||
0x5774756fu, 0x656e7469u, 0x00007373u, 0x00050005u, 0x0000003du, 0x6f6e6163u, 0x6163696eu, 0x00004e6cu,
|
||||
0x00060005u, 0x00000050u, 0x535f6c67u, 0x72676275u, 0x4970756fu, 0x00000044u, 0x00060005u, 0x0000005eu,
|
||||
0x535f6c67u, 0x72676275u, 0x5370756fu, 0x00657a69u, 0x00080005u, 0x00000064u, 0x535f6c67u, 0x72676275u,
|
||||
0x4970756fu, 0x636f766eu, 0x6f697461u, 0x0044496eu, 0x00060005u, 0x0000006eu, 0x6f6e6163u, 0x6163696eu,
|
||||
0x6d6f446cu, 0x006e6961u, 0x00070005u, 0x00000074u, 0x6e496469u, 0x6f6e6143u, 0x6163696eu, 0x6d6f446cu,
|
||||
0x006e6961u, 0x00060005u, 0x000000acu, 0x72756f73u, 0x6f446563u, 0x6e69616du, 0x00000000u, 0x00060005u,
|
||||
0x000000b7u, 0x706d6173u, 0x754c656cu, 0x616e696du, 0x0065636eu, 0x00060005u, 0x000000c8u, 0x66657270u,
|
||||
0x75537869u, 0x6361436du, 0x00006568u, 0x00050005u, 0x000000d9u, 0x706f6f6cu, 0x676e654cu, 0x00006874u,
|
||||
0x00050005u, 0x000000edu, 0x6e616373u, 0x67617453u, 0x00000065u, 0x00040047u, 0x0000000au, 0x0000000bu,
|
||||
0x0000001du, 0x00040047u, 0x0000002au, 0x0000000bu, 0x00000026u, 0x00040047u, 0x0000002du, 0x00000006u,
|
||||
0x00000004u, 0x00040047u, 0x0000002eu, 0x00000006u, 0x00000004u, 0x00040047u, 0x00000031u, 0x00000006u,
|
||||
0x00000008u, 0x00040047u, 0x00000032u, 0x00000006u, 0x00000008u, 0x00040047u, 0x00000034u, 0x00000006u,
|
||||
0x00000100u, 0x00030047u, 0x00000035u, 0x00000002u, 0x00050048u, 0x00000035u, 0x00000000u, 0x00000023u,
|
||||
0x00000000u, 0x00050048u, 0x00000035u, 0x00000001u, 0x00000023u, 0x00000004u, 0x00050048u, 0x00000035u,
|
||||
0x00000002u, 0x00000023u, 0x00000008u, 0x00050048u, 0x00000035u, 0x00000003u, 0x00000023u, 0x0000000cu,
|
||||
0x00050048u, 0x00000035u, 0x00000004u, 0x00000023u, 0x00000010u, 0x00050048u, 0x00000035u, 0x00000005u,
|
||||
0x00000023u, 0x00000020u, 0x00050048u, 0x00000035u, 0x00000006u, 0x00000023u, 0x00000030u, 0x00050048u,
|
||||
0x00000035u, 0x00000007u, 0x00000023u, 0x000000b0u, 0x00050048u, 0x00000035u, 0x00000008u, 0x00000023u,
|
||||
0x00000130u, 0x00050048u, 0x00000035u, 0x00000009u, 0x00000023u, 0x00000230u, 0x00050048u, 0x00000035u,
|
||||
0x0000000au, 0x00000023u, 0x00000830u, 0x00040047u, 0x00000037u, 0x00000021u, 0x00000000u, 0x00040047u,
|
||||
0x00000037u, 0x00000022u, 0x00000000u, 0x00040047u, 0x00000050u, 0x0000000bu, 0x00000028u, 0x00030047u,
|
||||
0x0000005eu, 0x00000000u, 0x00040047u, 0x0000005eu, 0x0000000bu, 0x00000024u, 0x00030047u, 0x0000005fu,
|
||||
0x00000000u, 0x00030047u, 0x00000064u, 0x00000000u, 0x00040047u, 0x00000064u, 0x0000000bu, 0x00000029u,
|
||||
0x00030047u, 0x00000065u, 0x00000000u, 0x00030047u, 0x00000066u, 0x00000000u, 0x00030047u, 0x00000087u,
|
||||
0x00000000u, 0x00030047u, 0x0000008bu, 0x00000000u, 0x00030047u, 0x0000008cu, 0x00000000u, 0x00030047u,
|
||||
0x0000008du, 0x00000000u, 0x00030047u, 0x000000c0u, 0x00000000u, 0x00030047u, 0x000000c1u, 0x00000000u,
|
||||
0x00030047u, 0x000000c2u, 0x00000000u, 0x00030047u, 0x00000107u, 0x00000000u, 0x00030047u, 0x00000108u,
|
||||
0x00000000u, 0x00030047u, 0x00000109u, 0x00000000u, 0x00030047u, 0x0000012fu, 0x00000000u, 0x00030047u,
|
||||
0x00000132u, 0x00000000u, 0x00040047u, 0x00000144u, 0x0000000bu, 0x00000019u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000000u, 0x00040020u,
|
||||
0x00000007u, 0x00000007u, 0x00000006u, 0x00040020u, 0x00000009u, 0x00000001u, 0x00000006u, 0x0004003bu,
|
||||
0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x00020014u,
|
||||
0x0000000eu, 0x00040020u, 0x00000012u, 0x00000004u, 0x00000006u, 0x0004003bu, 0x00000012u, 0x00000013u,
|
||||
0x00000004u, 0x0004003bu, 0x00000012u, 0x00000014u, 0x00000004u, 0x0004003bu, 0x00000012u, 0x00000015u,
|
||||
0x00000004u, 0x0004002bu, 0x00000006u, 0x00000017u, 0x00000020u, 0x0004001cu, 0x0000001bu, 0x00000006u,
|
||||
0x00000017u, 0x00040020u, 0x0000001cu, 0x00000004u, 0x0000001bu, 0x0004003bu, 0x0000001cu, 0x0000001du,
|
||||
0x00000004u, 0x0004003bu, 0x0000001cu, 0x00000020u, 0x00000004u, 0x0004002bu, 0x00000006u, 0x00000023u,
|
||||
0x00000001u, 0x0004002bu, 0x00000006u, 0x00000024u, 0x00000108u, 0x0004002bu, 0x00000006u, 0x00000025u,
|
||||
0x00000002u, 0x0004003bu, 0x00000009u, 0x0000002au, 0x00000001u, 0x00040017u, 0x0000002cu, 0x00000006u,
|
||||
0x00000004u, 0x0004001cu, 0x0000002du, 0x00000006u, 0x00000017u, 0x0004001cu, 0x0000002eu, 0x00000006u,
|
||||
0x00000017u, 0x00030016u, 0x0000002fu, 0x00000020u, 0x00040017u, 0x00000030u, 0x0000002fu, 0x00000002u,
|
||||
0x0004001cu, 0x00000031u, 0x00000030u, 0x00000017u, 0x0004001cu, 0x00000032u, 0x00000030u, 0x00000017u,
|
||||
0x0004002bu, 0x00000006u, 0x00000033u, 0x00000006u, 0x0004001cu, 0x00000034u, 0x00000032u, 0x00000033u,
|
||||
0x000d001eu, 0x00000035u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x0000002cu,
|
||||
0x0000002du, 0x0000002eu, 0x00000031u, 0x00000034u, 0x00000030u, 0x00040020u, 0x00000036u, 0x0000000cu,
|
||||
0x00000035u, 0x0004003bu, 0x00000036u, 0x00000037u, 0x0000000cu, 0x00040015u, 0x00000038u, 0x00000020u,
|
||||
0x00000001u, 0x0004002bu, 0x00000038u, 0x00000039u, 0x00000002u, 0x00040020u, 0x0000003bu, 0x0000000cu,
|
||||
0x00000006u, 0x0004003bu, 0x00000009u, 0x00000050u, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000005cu,
|
||||
0x00000004u, 0x0004003bu, 0x00000009u, 0x0000005eu, 0x00000001u, 0x0004003bu, 0x00000009u, 0x00000064u,
|
||||
0x00000001u, 0x0004002bu, 0x00000006u, 0x0000006bu, 0x00000008u, 0x00040020u, 0x0000006du, 0x00000007u,
|
||||
0x0000000eu, 0x0004002bu, 0x00000038u, 0x00000099u, 0x00000004u, 0x0004002bu, 0x00000038u, 0x000000a0u,
|
||||
0x00000006u, 0x0004002bu, 0x00000038u, 0x000000a6u, 0x00000007u, 0x00040020u, 0x000000b6u, 0x00000007u,
|
||||
0x00000030u, 0x0004002bu, 0x0000002fu, 0x000000bbu, 0x00000000u, 0x0004002bu, 0x00000006u, 0x000000beu,
|
||||
0x00000003u, 0x0004001cu, 0x000000c6u, 0x00000030u, 0x00000017u, 0x00040020u, 0x000000c7u, 0x00000004u,
|
||||
0x000000c6u, 0x0004003bu, 0x000000c7u, 0x000000c8u, 0x00000004u, 0x00040020u, 0x000000cbu, 0x00000004u,
|
||||
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000d2u, 0x00000008u, 0x00040020u, 0x000000d7u, 0x0000000cu,
|
||||
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000eau, 0x00000003u, 0x0004002bu, 0x00000038u, 0x00000115u,
|
||||
0x00000009u, 0x0004002bu, 0x00000038u, 0x0000011du, 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000120u,
|
||||
0x000001ffu, 0x0004002bu, 0x00000038u, 0x00000124u, 0x00000000u, 0x0004002bu, 0x0000002fu, 0x00000126u,
|
||||
0x44000000u, 0x0004002bu, 0x00000038u, 0x0000012eu, 0x00000005u, 0x00040020u, 0x00000134u, 0x0000000cu,
|
||||
0x0000002cu, 0x0004002bu, 0x00000038u, 0x00000136u, 0x0000000au, 0x0004002bu, 0x00000006u, 0x00000140u,
|
||||
0x50323033u, 0x00040017u, 0x00000142u, 0x00000006u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x00000143u,
|
||||
0x00000010u, 0x0006002cu, 0x00000142u, 0x00000144u, 0x00000017u, 0x00000143u, 0x00000023u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000007u,
|
||||
0x00000008u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x0000003du, 0x00000007u, 0x0004003bu, 0x0000006du,
|
||||
0x0000006eu, 0x00000007u, 0x0004003bu, 0x0000006du, 0x00000074u, 0x00000007u, 0x0004003bu, 0x0000006du,
|
||||
0x000000acu, 0x00000007u, 0x0004003bu, 0x000000b6u, 0x000000b7u, 0x00000007u, 0x0004003bu, 0x00000007u,
|
||||
0x000000d9u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x000000edu, 0x00000007u, 0x0004003du, 0x00000006u,
|
||||
0x0000000bu, 0x0000000au, 0x0003003eu, 0x00000008u, 0x0000000bu, 0x0004003du, 0x00000006u, 0x0000000cu,
|
||||
0x00000008u, 0x000500aau, 0x0000000eu, 0x0000000fu, 0x0000000cu, 0x0000000du, 0x000300f7u, 0x00000011u,
|
||||
0x00000000u, 0x000400fau, 0x0000000fu, 0x00000010u, 0x00000011u, 0x000200f8u, 0x00000010u, 0x0003003eu,
|
||||
0x00000013u, 0x0000000du, 0x0003003eu, 0x00000014u, 0x0000000du, 0x0003003eu, 0x00000015u, 0x0000000du,
|
||||
0x000200f9u, 0x00000011u, 0x000200f8u, 0x00000011u, 0x0004003du, 0x00000006u, 0x00000016u, 0x00000008u,
|
||||
0x000500b0u, 0x0000000eu, 0x00000018u, 0x00000016u, 0x00000017u, 0x000300f7u, 0x0000001au, 0x00000000u,
|
||||
0x000400fau, 0x00000018u, 0x00000019u, 0x0000001au, 0x000200f8u, 0x00000019u, 0x0004003du, 0x00000006u,
|
||||
0x0000001eu, 0x00000008u, 0x00050041u, 0x00000012u, 0x0000001fu, 0x0000001du, 0x0000001eu, 0x0003003eu,
|
||||
0x0000001fu, 0x0000000du, 0x0004003du, 0x00000006u, 0x00000021u, 0x00000008u, 0x00050041u, 0x00000012u,
|
||||
0x00000022u, 0x00000020u, 0x00000021u, 0x0003003eu, 0x00000022u, 0x0000000du, 0x000200f9u, 0x0000001au,
|
||||
0x000200f8u, 0x0000001au, 0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u,
|
||||
0x00000024u, 0x0004003du, 0x00000006u, 0x00000026u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000027u,
|
||||
0x00000026u, 0x0000000du, 0x000300f7u, 0x00000029u, 0x00000000u, 0x000400fau, 0x00000027u, 0x00000028u,
|
||||
0x00000029u, 0x000200f8u, 0x00000028u, 0x0004003du, 0x00000006u, 0x0000002bu, 0x0000002au, 0x0003003eu,
|
||||
0x00000013u, 0x0000002bu, 0x0004003du, 0x00000006u, 0x0000003au, 0x0000002au, 0x00050041u, 0x0000003bu,
|
||||
0x0000003cu, 0x00000037u, 0x00000039u, 0x0003003eu, 0x0000003cu, 0x0000003au, 0x000200f9u, 0x00000029u,
|
||||
0x000200f8u, 0x00000029u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u,
|
||||
0x0000003eu, 0x00000013u, 0x0003003eu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000006u, 0x0000003fu,
|
||||
0x0000002au, 0x0004003du, 0x00000006u, 0x00000040u, 0x0000003du, 0x000500abu, 0x0000000eu, 0x00000041u,
|
||||
0x0000003fu, 0x00000040u, 0x000300f7u, 0x00000043u, 0x00000000u, 0x000400fau, 0x00000041u, 0x00000042u,
|
||||
0x00000043u, 0x000200f8u, 0x00000042u, 0x000700f1u, 0x00000006u, 0x00000044u, 0x00000014u, 0x00000023u,
|
||||
0x0000000du, 0x00000023u, 0x000200f9u, 0x00000043u, 0x000200f8u, 0x00000043u, 0x0004003du, 0x00000006u,
|
||||
0x00000045u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000046u, 0x00000045u, 0x00000025u, 0x000400a8u,
|
||||
0x0000000eu, 0x00000047u, 0x00000046u, 0x000300f7u, 0x00000049u, 0x00000000u, 0x000400fau, 0x00000047u,
|
||||
0x00000048u, 0x00000049u, 0x000200f8u, 0x00000048u, 0x0004003du, 0x00000006u, 0x0000004au, 0x0000002au,
|
||||
0x000500acu, 0x0000000eu, 0x0000004bu, 0x0000004au, 0x00000017u, 0x000200f9u, 0x00000049u, 0x000200f8u,
|
||||
0x00000049u, 0x000700f5u, 0x0000000eu, 0x0000004cu, 0x00000046u, 0x00000043u, 0x0000004bu, 0x00000048u,
|
||||
0x000300f7u, 0x0000004eu, 0x00000000u, 0x000400fau, 0x0000004cu, 0x0000004du, 0x0000004eu, 0x000200f8u,
|
||||
0x0000004du, 0x000700f1u, 0x00000006u, 0x0000004fu, 0x00000014u, 0x00000023u, 0x0000000du, 0x00000025u,
|
||||
0x000200f9u, 0x0000004eu, 0x000200f8u, 0x0000004eu, 0x0004003du, 0x00000006u, 0x00000051u, 0x00000050u,
|
||||
0x0004003du, 0x00000006u, 0x00000052u, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000053u, 0x00000051u,
|
||||
0x00000052u, 0x000400a8u, 0x0000000eu, 0x00000054u, 0x00000053u, 0x000300f7u, 0x00000056u, 0x00000000u,
|
||||
0x000400fau, 0x00000054u, 0x00000055u, 0x00000056u, 0x000200f8u, 0x00000055u, 0x0004003du, 0x00000006u,
|
||||
0x00000057u, 0x00000050u, 0x000500aeu, 0x0000000eu, 0x00000058u, 0x00000057u, 0x00000017u, 0x000200f9u,
|
||||
0x00000056u, 0x000200f8u, 0x00000056u, 0x000700f5u, 0x0000000eu, 0x00000059u, 0x00000053u, 0x0000004eu,
|
||||
0x00000058u, 0x00000055u, 0x000300f7u, 0x0000005bu, 0x00000000u, 0x000400fau, 0x00000059u, 0x0000005au,
|
||||
0x0000005bu, 0x000200f8u, 0x0000005au, 0x000700f1u, 0x00000006u, 0x0000005du, 0x00000014u, 0x00000023u,
|
||||
0x0000000du, 0x0000005cu, 0x000200f9u, 0x0000005bu, 0x000200f8u, 0x0000005bu, 0x0004003du, 0x00000006u,
|
||||
0x0000005fu, 0x0000005eu, 0x000500aau, 0x0000000eu, 0x00000060u, 0x0000005fu, 0x0000000du, 0x000400a8u,
|
||||
0x0000000eu, 0x00000061u, 0x00000060u, 0x000300f7u, 0x00000063u, 0x00000000u, 0x000400fau, 0x00000061u,
|
||||
0x00000062u, 0x00000063u, 0x000200f8u, 0x00000062u, 0x0004003du, 0x00000006u, 0x00000065u, 0x00000064u,
|
||||
0x0004003du, 0x00000006u, 0x00000066u, 0x0000005eu, 0x000500aeu, 0x0000000eu, 0x00000067u, 0x00000065u,
|
||||
0x00000066u, 0x000200f9u, 0x00000063u, 0x000200f8u, 0x00000063u, 0x000700f5u, 0x0000000eu, 0x00000068u,
|
||||
0x00000060u, 0x0000005bu, 0x00000067u, 0x00000062u, 0x000300f7u, 0x0000006au, 0x00000000u, 0x000400fau,
|
||||
0x00000068u, 0x00000069u, 0x0000006au, 0x000200f8u, 0x00000069u, 0x000700f1u, 0x00000006u, 0x0000006cu,
|
||||
0x00000014u, 0x00000023u, 0x0000000du, 0x0000006bu, 0x000200f9u, 0x0000006au, 0x000200f8u, 0x0000006au,
|
||||
0x0004003du, 0x00000006u, 0x0000006fu, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000070u, 0x0000006fu,
|
||||
0x00000025u, 0x0004003du, 0x00000006u, 0x00000071u, 0x0000003du, 0x000500b2u, 0x0000000eu, 0x00000072u,
|
||||
0x00000071u, 0x00000017u, 0x000500a7u, 0x0000000eu, 0x00000073u, 0x00000070u, 0x00000072u, 0x0003003eu,
|
||||
0x0000006eu, 0x00000073u, 0x0004003du, 0x0000000eu, 0x00000075u, 0x0000006eu, 0x000300f7u, 0x00000077u,
|
||||
0x00000000u, 0x000400fau, 0x00000075u, 0x00000076u, 0x00000077u, 0x000200f8u, 0x00000076u, 0x0004003du,
|
||||
0x00000006u, 0x00000078u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000079u, 0x0000003du, 0x000500b0u,
|
||||
0x0000000eu, 0x0000007au, 0x00000078u, 0x00000079u, 0x000200f9u, 0x00000077u, 0x000200f8u, 0x00000077u,
|
||||
0x000700f5u, 0x0000000eu, 0x0000007bu, 0x00000075u, 0x0000006au, 0x0000007au, 0x00000076u, 0x0003003eu,
|
||||
0x00000074u, 0x0000007bu, 0x0004003du, 0x0000000eu, 0x0000007cu, 0x00000074u, 0x000300f7u, 0x0000007eu,
|
||||
0x00000000u, 0x000400fau, 0x0000007cu, 0x0000007du, 0x0000007eu, 0x000200f8u, 0x0000007du, 0x0004003du,
|
||||
0x00000006u, 0x0000007fu, 0x00000050u, 0x000500c4u, 0x00000006u, 0x00000080u, 0x00000023u, 0x0000007fu,
|
||||
0x000700f1u, 0x00000006u, 0x00000081u, 0x00000015u, 0x00000023u, 0x0000000du, 0x00000080u, 0x0004003du,
|
||||
0x00000006u, 0x00000082u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000083u, 0x00000020u, 0x00000082u,
|
||||
0x0004003du, 0x00000006u, 0x00000084u, 0x00000008u, 0x00050080u, 0x00000006u, 0x00000085u, 0x00000084u,
|
||||
0x00000023u, 0x000700eau, 0x00000006u, 0x00000086u, 0x00000083u, 0x00000023u, 0x0000000du, 0x00000085u,
|
||||
0x0004003du, 0x00000006u, 0x00000087u, 0x0000005eu, 0x000500abu, 0x0000000eu, 0x00000088u, 0x00000087u,
|
||||
0x0000000du, 0x000300f7u, 0x0000008au, 0x00000000u, 0x000400fau, 0x00000088u, 0x00000089u, 0x0000008au,
|
||||
0x000200f8u, 0x00000089u, 0x0004003du, 0x00000006u, 0x0000008bu, 0x00000064u, 0x0004003du, 0x00000006u,
|
||||
0x0000008cu, 0x0000005eu, 0x00050082u, 0x00000006u, 0x0000008du, 0x0000008cu, 0x00000023u, 0x000500aau,
|
||||
0x0000000eu, 0x0000008eu, 0x0000008bu, 0x0000008du, 0x000200f9u, 0x0000008au, 0x000200f8u, 0x0000008au,
|
||||
0x000700f5u, 0x0000000eu, 0x0000008fu, 0x00000088u, 0x0000007du, 0x0000008eu, 0x00000089u, 0x000300f7u,
|
||||
0x00000091u, 0x00000000u, 0x000400fau, 0x0000008fu, 0x00000090u, 0x00000091u, 0x000200f8u, 0x00000090u,
|
||||
0x0004003du, 0x00000006u, 0x00000092u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000093u, 0x0000001du,
|
||||
0x00000092u, 0x000700eau, 0x00000006u, 0x00000094u, 0x00000093u, 0x00000023u, 0x0000000du, 0x00000023u,
|
||||
0x000200f9u, 0x00000091u, 0x000200f8u, 0x00000091u, 0x000200f9u, 0x0000007eu, 0x000200f8u, 0x0000007eu,
|
||||
0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du,
|
||||
0x00000006u, 0x00000095u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000096u, 0x00000095u, 0x0000000du,
|
||||
0x000300f7u, 0x00000098u, 0x00000000u, 0x000400fau, 0x00000096u, 0x00000097u, 0x00000098u, 0x000200f8u,
|
||||
0x00000097u, 0x0004003du, 0x00000006u, 0x0000009au, 0x00000015u, 0x00050041u, 0x0000003bu, 0x0000009bu,
|
||||
0x00000037u, 0x00000099u, 0x0003003eu, 0x0000009bu, 0x0000009au, 0x000200f9u, 0x00000098u, 0x000200f8u,
|
||||
0x00000098u, 0x0004003du, 0x00000006u, 0x0000009cu, 0x00000008u, 0x000500b0u, 0x0000000eu, 0x0000009du,
|
||||
0x0000009cu, 0x00000017u, 0x000300f7u, 0x0000009fu, 0x00000000u, 0x000400fau, 0x0000009du, 0x0000009eu,
|
||||
0x0000009fu, 0x000200f8u, 0x0000009eu, 0x0004003du, 0x00000006u, 0x000000a1u, 0x00000008u, 0x0004003du,
|
||||
0x00000006u, 0x000000a2u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a3u, 0x0000001du, 0x000000a2u,
|
||||
0x0004003du, 0x00000006u, 0x000000a4u, 0x000000a3u, 0x00060041u, 0x0000003bu, 0x000000a5u, 0x00000037u,
|
||||
0x000000a0u, 0x000000a1u, 0x0003003eu, 0x000000a5u, 0x000000a4u, 0x0004003du, 0x00000006u, 0x000000a7u,
|
||||
0x00000008u, 0x0004003du, 0x00000006u, 0x000000a8u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a9u,
|
||||
0x00000020u, 0x000000a8u, 0x0004003du, 0x00000006u, 0x000000aau, 0x000000a9u, 0x00060041u, 0x0000003bu,
|
||||
0x000000abu, 0x00000037u, 0x000000a6u, 0x000000a7u, 0x0003003eu, 0x000000abu, 0x000000aau, 0x000200f9u,
|
||||
0x0000009fu, 0x000200f8u, 0x0000009fu, 0x0004003du, 0x0000000eu, 0x000000adu, 0x0000006eu, 0x000300f7u,
|
||||
0x000000afu, 0x00000000u, 0x000400fau, 0x000000adu, 0x000000aeu, 0x000000afu, 0x000200f8u, 0x000000aeu,
|
||||
0x0004003du, 0x00000006u, 0x000000b0u, 0x00000014u, 0x000500aau, 0x0000000eu, 0x000000b1u, 0x000000b0u,
|
||||
0x0000000du, 0x000200f9u, 0x000000afu, 0x000200f8u, 0x000000afu, 0x000700f5u, 0x0000000eu, 0x000000b2u,
|
||||
0x000000adu, 0x0000009fu, 0x000000b1u, 0x000000aeu, 0x0003003eu, 0x000000acu, 0x000000b2u, 0x0004003du,
|
||||
0x0000000eu, 0x000000b3u, 0x000000acu, 0x000300f7u, 0x000000b5u, 0x00000000u, 0x000400fau, 0x000000b3u,
|
||||
0x000000b4u, 0x000000b5u, 0x000200f8u, 0x000000b4u, 0x0004003du, 0x00000006u, 0x000000b8u, 0x0000000au,
|
||||
0x00050080u, 0x00000006u, 0x000000b9u, 0x000000b8u, 0x00000023u, 0x00040070u, 0x0000002fu, 0x000000bau,
|
||||
0x000000b9u, 0x00050050u, 0x00000030u, 0x000000bcu, 0x000000bau, 0x000000bbu, 0x0003003eu, 0x000000b7u,
|
||||
0x000000bcu, 0x0004003du, 0x00000030u, 0x000000bdu, 0x000000b7u, 0x0006015eu, 0x00000030u, 0x000000bfu,
|
||||
0x000000beu, 0x00000001u, 0x000000bdu, 0x0003003eu, 0x000000b7u, 0x000000bfu, 0x0004003du, 0x00000006u,
|
||||
0x000000c0u, 0x00000064u, 0x0004003du, 0x00000006u, 0x000000c1u, 0x0000005eu, 0x00050082u, 0x00000006u,
|
||||
0x000000c2u, 0x000000c1u, 0x00000023u, 0x000500aau, 0x0000000eu, 0x000000c3u, 0x000000c0u, 0x000000c2u,
|
||||
0x000300f7u, 0x000000c5u, 0x00000000u, 0x000400fau, 0x000000c3u, 0x000000c4u, 0x000000c5u, 0x000200f8u,
|
||||
0x000000c4u, 0x0004003du, 0x00000006u, 0x000000c9u, 0x00000050u, 0x0004003du, 0x00000030u, 0x000000cau,
|
||||
0x000000b7u, 0x00050041u, 0x000000cbu, 0x000000ccu, 0x000000c8u, 0x000000c9u, 0x0003003eu, 0x000000ccu,
|
||||
0x000000cau, 0x000200f9u, 0x000000c5u, 0x000200f8u, 0x000000c5u, 0x000400e0u, 0x00000025u, 0x00000025u,
|
||||
0x00000024u, 0x0004003du, 0x00000006u, 0x000000cdu, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000ceu,
|
||||
0x0000002au, 0x000500b0u, 0x0000000eu, 0x000000cfu, 0x000000cdu, 0x000000ceu, 0x000300f7u, 0x000000d1u,
|
||||
0x00000000u, 0x000400fau, 0x000000cfu, 0x000000d0u, 0x000000d1u, 0x000200f8u, 0x000000d0u, 0x0004003du,
|
||||
0x00000006u, 0x000000d3u, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000d4u, 0x0000000au, 0x00050041u,
|
||||
0x000000cbu, 0x000000d5u, 0x000000c8u, 0x000000d4u, 0x0004003du, 0x00000030u, 0x000000d6u, 0x000000d5u,
|
||||
0x00060041u, 0x000000d7u, 0x000000d8u, 0x00000037u, 0x000000d2u, 0x000000d3u, 0x0003003eu, 0x000000d8u,
|
||||
0x000000d6u, 0x000200f9u, 0x000000d1u, 0x000200f8u, 0x000000d1u, 0x000400e0u, 0x00000025u, 0x00000025u,
|
||||
0x00000024u, 0x0004003du, 0x00000006u, 0x000000dau, 0x0000002au, 0x0006000cu, 0x00000038u, 0x000000dbu,
|
||||
0x00000001u, 0x0000004bu, 0x000000dau, 0x0004007cu, 0x00000006u, 0x000000dcu, 0x000000dbu, 0x0003003eu,
|
||||
0x000000d9u, 0x000000dcu, 0x0004003du, 0x00000006u, 0x000000ddu, 0x0000002au, 0x0004003du, 0x00000006u,
|
||||
0x000000deu, 0x000000d9u, 0x00050082u, 0x00000006u, 0x000000dfu, 0x000000deu, 0x00000023u, 0x000500c4u,
|
||||
0x00000006u, 0x000000e0u, 0x00000023u, 0x000000dfu, 0x00050082u, 0x00000006u, 0x000000e1u, 0x000000ddu,
|
||||
0x000000e0u, 0x000500acu, 0x0000000eu, 0x000000e2u, 0x000000e1u, 0x0000000du, 0x000600a9u, 0x00000006u,
|
||||
0x000000e3u, 0x000000e2u, 0x00000023u, 0x0000000du, 0x0004003du, 0x00000006u, 0x000000e4u, 0x000000d9u,
|
||||
0x00050080u, 0x00000006u, 0x000000e5u, 0x000000e4u, 0x000000e3u, 0x0003003eu, 0x000000d9u, 0x000000e5u,
|
||||
0x0004003du, 0x00000006u, 0x000000e6u, 0x0000000au, 0x000500aau, 0x0000000eu, 0x000000e7u, 0x000000e6u,
|
||||
0x0000000du, 0x000300f7u, 0x000000e9u, 0x00000000u, 0x000400fau, 0x000000e7u, 0x000000e8u, 0x000000e9u,
|
||||
0x000200f8u, 0x000000e8u, 0x0004003du, 0x00000006u, 0x000000ebu, 0x000000d9u, 0x00050041u, 0x0000003bu,
|
||||
0x000000ecu, 0x00000037u, 0x000000eau, 0x0003003eu, 0x000000ecu, 0x000000ebu, 0x000200f9u, 0x000000e9u,
|
||||
0x000200f8u, 0x000000e9u, 0x0003003eu, 0x000000edu, 0x0000000du, 0x000200f9u, 0x000000eeu, 0x000200f8u,
|
||||
0x000000eeu, 0x000400f6u, 0x000000f0u, 0x000000f1u, 0x00000000u, 0x000200f9u, 0x000000f2u, 0x000200f8u,
|
||||
0x000000f2u, 0x0004003du, 0x00000006u, 0x000000f3u, 0x000000edu, 0x0004003du, 0x00000006u, 0x000000f4u,
|
||||
0x000000d9u, 0x000500b0u, 0x0000000eu, 0x000000f5u, 0x000000f3u, 0x000000f4u, 0x000400fau, 0x000000f5u,
|
||||
0x000000efu, 0x000000f0u, 0x000200f8u, 0x000000efu, 0x0004003du, 0x00000006u, 0x000000f6u, 0x00000050u,
|
||||
0x0004003du, 0x00000006u, 0x000000f7u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x000000f8u, 0x00000023u,
|
||||
0x000000f7u, 0x000500c7u, 0x00000006u, 0x000000f9u, 0x000000f6u, 0x000000f8u, 0x000500acu, 0x0000000eu,
|
||||
0x000000fau, 0x000000f9u, 0x0000000du, 0x000300f7u, 0x000000fcu, 0x00000000u, 0x000400fau, 0x000000fau,
|
||||
0x000000fbu, 0x000000fcu, 0x000200f8u, 0x000000fbu, 0x0004003du, 0x00000006u, 0x000000fdu, 0x00000050u,
|
||||
0x0004003du, 0x00000006u, 0x000000feu, 0x000000edu, 0x000500c2u, 0x00000006u, 0x000000ffu, 0x000000fdu,
|
||||
0x000000feu, 0x0004003du, 0x00000006u, 0x00000100u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x00000101u,
|
||||
0x000000ffu, 0x00000100u, 0x00050082u, 0x00000006u, 0x00000102u, 0x00000101u, 0x00000023u, 0x00050041u,
|
||||
0x000000cbu, 0x00000103u, 0x000000c8u, 0x00000102u, 0x0004003du, 0x00000030u, 0x00000104u, 0x00000103u,
|
||||
0x0004003du, 0x00000030u, 0x00000105u, 0x000000b7u, 0x00050081u, 0x00000030u, 0x00000106u, 0x00000105u,
|
||||
0x00000104u, 0x0003003eu, 0x000000b7u, 0x00000106u, 0x0004003du, 0x00000006u, 0x00000107u, 0x00000064u,
|
||||
0x0004003du, 0x00000006u, 0x00000108u, 0x0000005eu, 0x00050082u, 0x00000006u, 0x00000109u, 0x00000108u,
|
||||
0x00000023u, 0x000500aau, 0x0000000eu, 0x0000010au, 0x00000107u, 0x00000109u, 0x000300f7u, 0x0000010cu,
|
||||
0x00000000u, 0x000400fau, 0x0000010au, 0x0000010bu, 0x0000010cu, 0x000200f8u, 0x0000010bu, 0x0004003du,
|
||||
0x00000006u, 0x0000010du, 0x00000050u, 0x0004003du, 0x00000030u, 0x0000010eu, 0x000000b7u, 0x00050041u,
|
||||
0x000000cbu, 0x0000010fu, 0x000000c8u, 0x0000010du, 0x0003003eu, 0x0000010fu, 0x0000010eu, 0x000200f9u,
|
||||
0x0000010cu, 0x000200f8u, 0x0000010cu, 0x000200f9u, 0x000000fcu, 0x000200f8u, 0x000000fcu, 0x000400e0u,
|
||||
0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x00000110u, 0x0000000au, 0x0004003du,
|
||||
0x00000006u, 0x00000111u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000112u, 0x00000110u, 0x00000111u,
|
||||
0x000300f7u, 0x00000114u, 0x00000000u, 0x000400fau, 0x00000112u, 0x00000113u, 0x00000114u, 0x000200f8u,
|
||||
0x00000113u, 0x0004003du, 0x00000006u, 0x00000116u, 0x000000edu, 0x0004003du, 0x00000006u, 0x00000117u,
|
||||
0x0000000au, 0x0004003du, 0x00000006u, 0x00000118u, 0x0000000au, 0x00050041u, 0x000000cbu, 0x00000119u,
|
||||
0x000000c8u, 0x00000118u, 0x0004003du, 0x00000030u, 0x0000011au, 0x00000119u, 0x00070041u, 0x000000d7u,
|
||||
0x0000011bu, 0x00000037u, 0x00000115u, 0x00000116u, 0x00000117u, 0x0003003eu, 0x0000011bu, 0x0000011au,
|
||||
0x000200f9u, 0x00000114u, 0x000200f8u, 0x00000114u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u,
|
||||
0x000200f9u, 0x000000f1u, 0x000200f8u, 0x000000f1u, 0x0004003du, 0x00000006u, 0x0000011cu, 0x000000edu,
|
||||
0x00050080u, 0x00000006u, 0x0000011eu, 0x0000011cu, 0x0000011du, 0x0003003eu, 0x000000edu, 0x0000011eu,
|
||||
0x000200f9u, 0x000000eeu, 0x000200f8u, 0x000000f0u, 0x0004003du, 0x00000006u, 0x0000011fu, 0x0000000au,
|
||||
0x000500aau, 0x0000000eu, 0x00000121u, 0x0000011fu, 0x00000120u, 0x000300f7u, 0x00000123u, 0x00000000u,
|
||||
0x000400fau, 0x00000121u, 0x00000122u, 0x00000123u, 0x000200f8u, 0x00000122u, 0x0004003du, 0x00000030u,
|
||||
0x00000125u, 0x000000b7u, 0x00050050u, 0x00000030u, 0x00000127u, 0x00000126u, 0x00000126u, 0x00050088u,
|
||||
0x00000030u, 0x00000128u, 0x00000125u, 0x00000127u, 0x00050041u, 0x000000cbu, 0x00000129u, 0x000000c8u,
|
||||
0x00000124u, 0x0003003eu, 0x00000129u, 0x00000128u, 0x000200f9u, 0x00000123u, 0x000200f8u, 0x00000123u,
|
||||
0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000012au, 0x0000000au,
|
||||
0x000500aau, 0x0000000eu, 0x0000012bu, 0x0000012au, 0x00000120u, 0x000300f7u, 0x0000012du, 0x00000000u,
|
||||
0x000400fau, 0x0000012bu, 0x0000012cu, 0x0000012du, 0x000200f8u, 0x0000012cu, 0x0004003du, 0x00000006u,
|
||||
0x0000012fu, 0x0000005eu, 0x0004003du, 0x00000006u, 0x00000130u, 0x0000002au, 0x0004003du, 0x00000006u,
|
||||
0x00000131u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000132u, 0x00000064u, 0x00070050u, 0x0000002cu,
|
||||
0x00000133u, 0x0000012fu, 0x00000130u, 0x00000131u, 0x00000132u, 0x00050041u, 0x00000134u, 0x00000135u,
|
||||
0x00000037u, 0x0000012eu, 0x0003003eu, 0x00000135u, 0x00000133u, 0x00050041u, 0x000000cbu, 0x00000137u,
|
||||
0x000000c8u, 0x00000124u, 0x0004003du, 0x00000030u, 0x00000138u, 0x00000137u, 0x00050041u, 0x000000d7u,
|
||||
0x00000139u, 0x00000037u, 0x00000136u, 0x0003003eu, 0x00000139u, 0x00000138u, 0x000200f9u, 0x0000012du,
|
||||
0x000200f8u, 0x0000012du, 0x000200f9u, 0x000000b5u, 0x000200f8u, 0x000000b5u, 0x000400e0u, 0x00000025u,
|
||||
0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000013au, 0x00000008u, 0x000500aau, 0x0000000eu,
|
||||
0x0000013bu, 0x0000013au, 0x0000000du, 0x000300f7u, 0x0000013du, 0x00000000u, 0x000400fau, 0x0000013bu,
|
||||
0x0000013cu, 0x0000013du, 0x000200f8u, 0x0000013cu, 0x0004003du, 0x00000006u, 0x0000013eu, 0x00000014u,
|
||||
0x00050041u, 0x0000003bu, 0x0000013fu, 0x00000037u, 0x0000011du, 0x0003003eu, 0x0000013fu, 0x0000013eu,
|
||||
0x00050041u, 0x0000003bu, 0x00000141u, 0x00000037u, 0x00000124u, 0x0003003eu, 0x00000141u, 0x00000140u,
|
||||
0x000200f9u, 0x0000013du, 0x000200f8u, 0x0000013du, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
inline constexpr std::size_t kDriverPostIterationRPWitnessSpvWordCount =
|
||||
sizeof(kDriverPostIterationRPWitnessSpv) / sizeof(kDriverPostIterationRPWitnessSpv[0]);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -38,7 +38,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
HashBytes(state, env.advertisedExtensions.data(),
|
||||
env.advertisedExtensions.size() * sizeof(GLExtension));
|
||||
}
|
||||
HashValue(state, env.subgroupPrefixScanQuirk);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -73,8 +72,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
kFrontendMaxComputeWorkGroupInvocations)
|
||||
: kFrontendMaxComputeWorkGroupInvocations;
|
||||
|
||||
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
|
||||
|
||||
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
||||
return env;
|
||||
}
|
||||
@@ -84,7 +81,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// computed, and this must not run before MG_Config is loaded.
|
||||
static const SharedPtr<const CompileEnv> kDefault = [] {
|
||||
auto env = MakeShared<CompileEnv>();
|
||||
env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk;
|
||||
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
||||
return SharedPtr<const CompileEnv>(Move(env));
|
||||
}();
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// Everything the shader compile/link pipeline reads from OUTSIDE its own (stage, source)
|
||||
// inputs: backend identity, backend limits, the advertised extension list, and the one
|
||||
// config quirk the source rewriter branches on.
|
||||
// everything outside (stage, source) this reads - advertised extensions and backend limits -
|
||||
// so the transformation is a pure function of its three arguments and can run on a worker
|
||||
// thread.
|
||||
//
|
||||
// Why it exists (P1): every one of those reads is a reach-back into
|
||||
// MG_Backend::pActiveBackendObject / gBackendFunctionsTable, and one of them
|
||||
@@ -46,9 +46,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
MG_Backend::DynamicBackendParameters params{}; // by value, never by reference
|
||||
Vector<GLExtension> advertisedExtensions;
|
||||
|
||||
// --- config the source rewriter branches on ---
|
||||
MG_Config::QuirkOverride subgroupPrefixScanQuirk = MG_Config::QuirkOverride::Auto;
|
||||
|
||||
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
|
||||
|
||||
Bool HasBackend() const { return backend != BackendType::Unknown; }
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace MobileGL {
|
||||
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
|
||||
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
|
||||
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
|
||||
"ldexp", "length", "lessThan", "lessThanEqual", "log", "log2",
|
||||
"ldexp", "length", "length_squared", "lessThan", "lessThanEqual", "log", "log2",
|
||||
"matrixCompMult", "max", "max3", "memoryBarrier",
|
||||
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
|
||||
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
||||
#include "SpirvPasses/EmulateSubgroupsPass.h"
|
||||
#include "SpirvPasses/FixIterationRPBarrierPass.h"
|
||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
@@ -369,12 +373,6 @@ namespace MobileGL {
|
||||
return allSpirv;
|
||||
}
|
||||
|
||||
// -1 unresolved, 0 off, 1 on. Resolved once from MOBILEGL_VALIDATE_SPIRV on first
|
||||
// use. A live getenv rather than an MG_Config::Features field, for the same reason
|
||||
// Config.h already exempts MOBILEGL_LOG_FILE_PATH: suites like SpirvPassTest never
|
||||
// run MobileGL::Initialize(), and every Initialize() re-runs MG_ConfigLoader::Init,
|
||||
// which would clobber a programmatic override stored in the feature table.
|
||||
static std::atomic<int> g_validateSpirv{-1};
|
||||
// Total validation failures observed this process. This latch - not the wrappers'
|
||||
// return values - is the test-lane signal: validation must never change what a
|
||||
// wrapper returns, or the validating lanes would render differently from the
|
||||
@@ -383,28 +381,6 @@ namespace MobileGL {
|
||||
static std::atomic<Uint64> g_spirvValidationFailures{0};
|
||||
|
||||
namespace {
|
||||
// Test lanes (desktop/CI/WSL) validate by default; device builds do not -
|
||||
// validation costs real time per module, and on device the driver is the
|
||||
// final validator anyway. MOBILEGL_VALIDATE_SPIRV overrides in either
|
||||
// direction, using the ConfigLoader truthy rule.
|
||||
constexpr bool kValidateSpirvDefault =
|
||||
#if defined(__ANDROID__)
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
bool IsTruthySpirvEnvValue(const char* value) {
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
String lowered(value);
|
||||
for (auto& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return lowered != "0" && lowered != "false";
|
||||
}
|
||||
|
||||
// spirv-tools' validator lazily constructs function-local static tables on
|
||||
// its first run, which on this codebase happens on a ShaderCompilePool
|
||||
// worker. Function-local statics are destroyed in reverse construction
|
||||
@@ -445,11 +421,6 @@ namespace MobileGL {
|
||||
tools.Validate(warmup);
|
||||
}
|
||||
std::atexit(+[] {
|
||||
// Flip validation off first: a validator table this warmup does
|
||||
// not know about (a future spirv-tools bump) would still be
|
||||
// destroyed before this handler, and workers must stop entering
|
||||
// Validate before the drain waits for them.
|
||||
g_validateSpirv.store(0, std::memory_order_release);
|
||||
Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
||||
});
|
||||
});
|
||||
@@ -478,10 +449,10 @@ namespace MobileGL {
|
||||
// Validation is decoupled from control flow on purpose: a failure logs and
|
||||
// bumps the latch, and the caller proceeds exactly as the shipping (non-
|
||||
// validating) configuration would. Tests assert on the latch delta.
|
||||
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary) {
|
||||
if (!ShaderCompiler::SpirvValidationEnabled()) {
|
||||
return;
|
||||
}
|
||||
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary,
|
||||
const bool enableSpirvValidation) {
|
||||
if (!enableSpirvValidation) return;
|
||||
PinValidatorTablesForProcessExit();
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!tools.Validate(binary)) {
|
||||
@@ -502,39 +473,23 @@ namespace MobileGL {
|
||||
// spirv-tools drops pass diagnostics on the floor.
|
||||
bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer,
|
||||
const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary, const bool validateOutput,
|
||||
const bool enableSpirvValidation) {
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) {
|
||||
return false;
|
||||
}
|
||||
ValidateOrLatch(site, outputBinary);
|
||||
if (validateOutput) {
|
||||
ValidateOrLatch(site, outputBinary, enableSpirvValidation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ShaderCompiler::SpirvValidationEnabled() {
|
||||
int state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state < 0) {
|
||||
const char* env = std::getenv("MOBILEGL_VALIDATE_SPIRV");
|
||||
const bool resolved = env != nullptr ? IsTruthySpirvEnvValue(env) : kValidateSpirvDefault;
|
||||
int expected = -1;
|
||||
g_validateSpirv.compare_exchange_strong(expected, resolved ? 1 : 0,
|
||||
std::memory_order_acq_rel);
|
||||
state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state == 1) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
}
|
||||
return state == 1;
|
||||
}
|
||||
|
||||
void ShaderCompiler::SetSpirvValidationEnabled(bool enabled) {
|
||||
g_validateSpirv.store(enabled ? 1 : 0, std::memory_order_release);
|
||||
if (enabled) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
void ShaderCompiler::PrepareSpirvValidation() {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
|
||||
Uint64 ShaderCompiler::NoteSpirvValidationFailure() {
|
||||
@@ -604,16 +559,19 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary);
|
||||
return RunOptimizerChecked("DemoteFloat64ToFloat32", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool validateOutput,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
|
||||
@@ -663,38 +621,41 @@ namespace MobileGL {
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, validateOutput, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
|
||||
|
||||
return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass());
|
||||
|
||||
return RunOptimizerChecked("SplitArrayVertexInputsForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::BakeImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
const UnorderedMap<String, Uint>& glFormatByName,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
if (glFormatByName.empty()) return false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(BakeImageFormatsPass::CreateBakeImageFormatsPass(glFormatByName));
|
||||
|
||||
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary);
|
||||
return RunOptimizerChecked("BakeImageFormatsForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresFormatlessStorageImage(const Vector<Uint32>& binary) {
|
||||
@@ -718,7 +679,8 @@ namespace MobileGL {
|
||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
if (blockNames.empty()) return false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
@@ -726,7 +688,7 @@ namespace MobileGL {
|
||||
blockNames, &flattenedBlockNames));
|
||||
|
||||
return RunOptimizerChecked("FlattenXfbInterfaceBlocksForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
||||
@@ -736,48 +698,53 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
||||
|
||||
return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
||||
|
||||
return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer,
|
||||
inputBinary, outputBinary);
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||
|
||||
return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||
|
||||
return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Detection gates everything: a module with no dynamically indexed fragment
|
||||
@@ -810,7 +777,7 @@ namespace MobileGL {
|
||||
|
||||
Vector<uint32_t> folded;
|
||||
if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.fold", folder, inputBinary,
|
||||
folded) ||
|
||||
folded, true, enableSpirvValidation) ||
|
||||
folded.empty()) {
|
||||
// Fail open onto the fallback rather than onto the illegal module.
|
||||
folded = inputBinary;
|
||||
@@ -829,7 +796,7 @@ namespace MobileGL {
|
||||
lowerer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
|
||||
if (!RunOptimizerChecked("LegalizeFragmentOutputIndexingForEssl.lower", lowerer, folded,
|
||||
outputBinary) ||
|
||||
outputBinary, true, enableSpirvValidation) ||
|
||||
outputBinary.empty()) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
@@ -846,16 +813,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
|
||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
|
||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Declined rather than half-translated: after the rewrite the image is a 2D
|
||||
@@ -897,40 +865,90 @@ namespace MobileGL {
|
||||
// second Shader. Deduplicating afterwards collapses all three at once.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary);
|
||||
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
|
||||
|
||||
return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(ZeroBaseVertexPass::CreateZeroBaseVertexPass());
|
||||
|
||||
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary);
|
||||
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass());
|
||||
|
||||
return RunOptimizerChecked("DeriveNumSubgroupsForVulkan", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const Uint32 maxWorkgroupScratchBytes,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes));
|
||||
|
||||
return RunOptimizerChecked("EmulateSubgroupsForVulkan", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
||||
nativeSubgroupSize, maxWorkgroupScratchBytes));
|
||||
|
||||
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
|
||||
|
||||
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
||||
|
||||
return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||
outputBinary.clear();
|
||||
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||
@@ -1058,7 +1076,8 @@ namespace MobileGL {
|
||||
addedCapabilities.begin(), addedCapabilities.end());
|
||||
// Hand-rolled word walk, so no Optimizer wrapper ever sees this rewrite;
|
||||
// check the modified module explicitly in validating lanes.
|
||||
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary);
|
||||
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary,
|
||||
enableSpirvValidation);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user