mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
171
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f8adcf6d | ||
|
|
795e08f7e6 | ||
|
|
1e7ecab4db | ||
|
|
81b17c0b75 | ||
|
|
d1edf765f5 | ||
|
|
97e07190ac | ||
|
|
a4dcdf989e | ||
|
|
1c113e4b26 | ||
|
|
bf9cfb3079 | ||
|
|
e1818d497a | ||
|
|
d7f66722d1 | ||
|
|
92dc41ebf9 | ||
|
|
feea131d8b | ||
|
|
19f4402fbf | ||
|
|
1350031368 | ||
|
|
0ee3384b22 | ||
|
|
ba3f8d6774 | ||
|
|
3327784fd0 | ||
|
|
ff426da3a9 | ||
|
|
08419a1fe6 | ||
|
|
5d51372c44 | ||
|
|
7fd4550968 | ||
|
|
971537058e | ||
|
|
dd98c450ad | ||
|
|
5dbbbbd7eb | ||
|
|
5d140a41ce | ||
|
|
bf376b230f | ||
|
|
29599dcf90 | ||
|
|
734fab9f90 | ||
|
|
2cd1809c29 | ||
|
|
faed498476 | ||
|
|
8282eecbfa | ||
|
|
f4f3afb0b6 | ||
|
|
a28da07641 | ||
|
|
200c21336f | ||
|
|
2c3fc583d5 | ||
|
|
645a12d8bc | ||
|
|
8e6acc5528 | ||
|
|
525ffe0f14 | ||
|
|
ad28d2b744 | ||
|
|
eab622388f | ||
|
|
75e573c923 | ||
|
|
0d0ef13619 | ||
|
|
23565fcacd | ||
|
|
5dc26e3e2c | ||
|
|
90564aa82e | ||
|
|
7520607d47 | ||
|
|
57635a9198 | ||
|
|
c19d0f0b75 | ||
|
|
e42e7d00f5 | ||
|
|
62695ee3c2 | ||
|
|
02cc0ce83c | ||
|
|
9e52a0b23e | ||
|
|
9dee53337f | ||
|
|
28c5badf8f | ||
|
|
01116f7b41 | ||
|
|
05d627ba2d | ||
|
|
ea5d52f126 | ||
|
|
3c70b4fc0f | ||
|
|
b6d6316333 | ||
|
|
3c9ab5a68f | ||
|
|
532b5e9cc5 | ||
|
|
06605ed0ea | ||
|
|
e1d5bdc4a5 | ||
|
|
66867a41ba | ||
|
|
ebff4b21f7 | ||
|
|
d4e7378868 | ||
|
|
01d20e5c96 | ||
|
|
b04c67d9a8 | ||
|
|
685d83e3ec | ||
|
|
009b140691 | ||
|
|
1f44e5bc1d | ||
|
|
d9aebcba26 | ||
|
|
0db666897e | ||
|
|
5f445e499f | ||
|
|
c52ebd5bf6 | ||
|
|
8e072bc793 | ||
|
|
88ee75be0e | ||
|
|
0dbb4ceba8 | ||
|
|
a94b3e0bd5 | ||
|
|
764b6e044d | ||
|
|
c1d89de729 | ||
|
|
c136384f97 | ||
|
|
1920a3d16f | ||
|
|
11f4b4bd3b | ||
|
|
9ef33f4274 | ||
|
|
e430e1b3be | ||
|
|
e315d9e798 | ||
|
|
6f299372c6 | ||
|
|
be7bf21eb8 | ||
|
|
0e4302b399 | ||
|
|
c9c2dcb42a | ||
|
|
2d938971b9 | ||
|
|
f7e23d5d83 | ||
|
|
02fbb816e9 | ||
|
|
0e0882cfc6 | ||
|
|
de09646d5e | ||
|
|
747864777e | ||
|
|
6fc3504bd9 | ||
|
|
df1bcdba09 | ||
|
|
843c61dee1 | ||
|
|
c0a3f4cc50 | ||
|
|
06744fde7f | ||
|
|
6cc9faf772 | ||
|
|
7168f2ef77 | ||
|
|
07669aacd4 | ||
|
|
d52a3b2196 | ||
|
|
9e23016dd4 | ||
|
|
f1354dc25e | ||
|
|
b7a694711a | ||
|
|
2ae848ca19 | ||
|
|
acf86d1fb6 | ||
|
|
07a0408a28 | ||
|
|
8cf2e2aea9 | ||
|
|
31252cf0da | ||
|
|
2635fe84b6 | ||
|
|
e3163233a5 | ||
|
|
eb9e4fdac1 | ||
|
|
90b7a689c5 | ||
|
|
e69e939d1a | ||
|
|
b675e2a0b0 | ||
|
|
6979926a6f | ||
|
|
d5286e69b6 | ||
|
|
0f2fcbc469 | ||
|
|
18fccdd796 | ||
|
|
0d2fceab0e | ||
|
|
82decded58 | ||
|
|
d04a3394de | ||
|
|
33eabfc2ff | ||
|
|
56e5d13dc9 | ||
|
|
34b7cc772f | ||
|
|
c1d6a3c908 | ||
|
|
386bd7e461 | ||
|
|
12df061e0b | ||
|
|
d83a48da5c | ||
|
|
b3100b0de5 | ||
|
|
1cde801a01 | ||
|
|
52c050131e | ||
|
|
ebb8a4cebf | ||
|
|
5398fb4289 | ||
|
|
7f2ca68615 | ||
|
|
b12ef4d717 | ||
|
|
724755d9df | ||
|
|
59f7059bf4 | ||
|
|
4446c861be | ||
|
|
602da1d131 | ||
|
|
06bbaf32b1 | ||
|
|
9bcf0a15a0 | ||
|
|
26e5a946ac | ||
|
|
dc543fa905 | ||
|
|
f559d68728 | ||
|
|
48968a663f | ||
|
|
0fdcb5d6c4 | ||
|
|
6b25e7a7e3 | ||
|
|
50d260c840 | ||
|
|
6a8bf4c03c | ||
|
|
01098e9dd7 | ||
|
|
82244e9048 | ||
|
|
7bd2f08313 | ||
|
|
121b99f8c3 | ||
|
|
e8d79344d6 | ||
|
|
88be35c9ba | ||
|
|
60808b6cf2 | ||
|
|
eb2f14e55a | ||
|
|
0eb5d54bb8 | ||
|
|
6a2e9dc791 | ||
|
|
d5aceebd7b | ||
|
|
473d9951b7 | ||
|
|
13783e3aec | ||
|
|
6bb844b1c1 | ||
|
|
6162603072 |
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
|
||||
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
|
||||
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
||||
|
||||
@@ -417,12 +417,12 @@ jobs:
|
||||
|
||||
- name: Retrace and validate
|
||||
env:
|
||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_ESPRYT_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' }}
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_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,16 +265,26 @@ 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"
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
# Second, filtered pass: with the range-invalidating map flush disabled,
|
||||
# the buffer scenarios run on the upload ring's staged-copy tier - which
|
||||
# the default pass never reaches (the map tier absorbs every flush on
|
||||
# Mesa), so without this the Mali fallback tier would have zero CI
|
||||
# coverage. The flag is NOT baked into the ctest ENVIRONMENT properties,
|
||||
# so an inline env reaches the test processes (unlike the ICD pin above).
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L integration-gpu --no-tests=error
|
||||
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest -V -L integration-gpu \
|
||||
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
||||
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --output-on-failure -L integration-gpu \
|
||||
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
@@ -644,9 +654,9 @@ jobs:
|
||||
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
|
||||
export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||
export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1
|
||||
export MOBILEGL_MAGMA_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
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: fa562bb911...d89cf443bc
@@ -285,6 +285,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
@@ -306,6 +307,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -313,6 +315,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
@@ -335,6 +338,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
|
||||
@@ -392,6 +396,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
|
||||
+112
-21
@@ -69,27 +69,34 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EsprytEnableTextureView = 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;
|
||||
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool EsprytUseAngle = 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_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool DisableSubgroup = false;
|
||||
Bool MagmaDisableSubgroup = 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.
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// MOBILEGL_MAGMA_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
|
||||
@@ -99,12 +106,12 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_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
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_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
|
||||
@@ -112,7 +119,7 @@ namespace MobileGL::MG_Config {
|
||||
// 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;
|
||||
QuirkOverride MagmaDeriveNumSubgroups = 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
|
||||
@@ -125,16 +132,39 @@ namespace MobileGL::MG_Config {
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool AvoidExplicitLodBias = false;
|
||||
Bool EsprytAvoidExplicitLodBias = false;
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
|
||||
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
|
||||
// match them by block name and member sequence instead. The Mali ES driver delivers
|
||||
// nothing at all through a located block once a tessellation or geometry stage is in
|
||||
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
|
||||
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
|
||||
// healthy driver - which is what the integration lane does, since llvmpipe and
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -144,10 +174,38 @@ namespace MobileGL::MG_Config {
|
||||
Bool CoherentAsFlush = false;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
Bool TraceSkipAutodestroy = false;
|
||||
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
Bool EsprytDisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
// glTexSubImage from the client pointer instead of staging them through the
|
||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||
// hatch).
|
||||
Bool EsprytDisableUnpackRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
|
||||
// (glBufferSubData / map flushes) back to the immediate driver upload instead
|
||||
// of queueing them for the staged-copy flush through the persistent-mapped
|
||||
// upload ring (negative control / driver-bug escape hatch; the immediate
|
||||
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
|
||||
Bool EsprytDisableUploadRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
|
||||
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
|
||||
// to the upload ring's staged glCopyBufferSubData (negative control / escape
|
||||
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
|
||||
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
@@ -168,10 +226,10 @@ namespace MobileGL::MG_Config {
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
Bool MagmaDisableRobustBufferAccess = false;
|
||||
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
@@ -212,7 +270,7 @@ namespace MobileGL::MG_Config {
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
@@ -224,7 +282,40 @@ namespace MobileGL::MG_Config {
|
||||
// the pre-emulation path, extension passthrough where it exists and
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
||||
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
|
||||
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
|
||||
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
|
||||
// packed16 allocations with a MIRRORED field order (allocation-scoped and
|
||||
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
|
||||
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
|
||||
// reversed whenever exactly one endpoint sits in a mirrored allocation
|
||||
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
|
||||
// With no 16-bit packed ES image left there is no field order to disagree about; the
|
||||
// client word still round-trips exactly, because the canonical shadow is already
|
||||
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
|
||||
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
|
||||
// path); ForceOff keeps the native narrow storage even where the probe fires - the
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||
// the same capture-less draws and arms the best proven one: the dedicated
|
||||
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+23
-13
@@ -162,32 +162,39 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_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.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_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);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
@@ -195,8 +202,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.ViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -301,6 +301,12 @@ namespace MobileGL {
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
SizeT ShaderStorageBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
@@ -383,6 +389,19 @@ namespace MobileGL {
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
// backend that cannot host a cull distance MUST report it. The failure this prevents
|
||||
// is worse than the clip one, because cull distance discards the whole primitive:
|
||||
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
|
||||
// gl_MaxCullDistances from it, SPIRV-Cross then emits
|
||||
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
|
||||
// without that extension rejects the program in an info log nobody surfaces. These
|
||||
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
|
||||
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
|
||||
// is: they describe the no-backend case (standalone compiles, unit tests).
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||
@@ -476,6 +495,24 @@ namespace MobileGL {
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||
// independently from the EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||
// varying program-wide (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||
// overrides the detection in either direction at backend init.
|
||||
//
|
||||
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||
// sets it gets standard modules and, at worst, the old honest declines.
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -307,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return capabilities.MaxColorTextureSamples;
|
||||
}
|
||||
|
||||
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
|
||||
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
|
||||
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
|
||||
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
|
||||
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
|
||||
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
|
||||
// in the GLES loader) and the walk below would never run past one sample, recording {1}
|
||||
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
|
||||
// reports 4. Once the frontend validates against this list, that would reject every
|
||||
// multisample renderbuffer on such a context.
|
||||
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
|
||||
GLenum imageFormat) {
|
||||
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
|
||||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
|
||||
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
||||
GLuint texture, TextureInternalFormat format) {
|
||||
GLuint framebuffer = 0;
|
||||
@@ -717,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat));
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
} else {
|
||||
@@ -731,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
@@ -749,11 +766,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLVersion = {4, 6, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -777,7 +794,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
|
||||
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -990,9 +1008,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
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_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,
|
||||
@@ -1017,6 +1042,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||
// runs on provides.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
|
||||
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
|
||||
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
|
||||
// buffer the application bound. DirectVulkan reaches the same place through its own
|
||||
// descriptor resolution, so the string is symmetric.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
|
||||
// object-label table are MobileGL's own state, not the host driver's - so it is as
|
||||
// available here as it is on DirectVulkan, which has advertised it all along.
|
||||
E_GL_KHR_debug,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
|
||||
// driver's identical parameters.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
@@ -1065,6 +1161,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
|
||||
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
|
||||
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
|
||||
// reports. So the string follows the host capability rather than the version.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
|
||||
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
|
||||
// over the SAME storage, so that writes through either are visible through the other and
|
||||
// the two carry independent per-texture parameters at the same time - which is exactly
|
||||
// what applications use it for (Better Clouds samples one D24S8 through its own name with
|
||||
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
|
||||
// a single shading pass). A copy-based fallback satisfies neither half, and fails
|
||||
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
|
||||
// the only behaviour that cannot be mistaken for success.
|
||||
//
|
||||
// The host extension is necessary and NOT sufficient, which is why this second gate
|
||||
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
|
||||
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
|
||||
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
|
||||
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
|
||||
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
|
||||
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
|
||||
// land on different renderability carriers the host rejects the pair, the error is
|
||||
// swallowed, and the view is left as a storage-less name that samples as zeros.
|
||||
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
|
||||
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
|
||||
//
|
||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||
// above refuses to tell, just with an extra prerequisite met. Set
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
|
||||
extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
// state is accepted regardless, but forwarding it would be a no-op without the extension,
|
||||
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
|
||||
@@ -1204,6 +1341,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
|
||||
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||
@@ -1340,9 +1479,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
|
||||
// extension pairs the loader probed, independently, because they really do come
|
||||
// separately. False arms the shared phase-B demotion
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
|
||||
// never names the built-in in those stages and needs no extension.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
|
||||
// exercised on a healthy driver (the pinned integration lane); =0 restores the
|
||||
// detected answer's declines.
|
||||
m_dynamicParameters.SupportsTessellationPointSize =
|
||||
m_GLESCapabilities.TessellationPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
m_dynamicParameters.SupportsGeometryPointSize =
|
||||
m_GLESCapabilities.GeometryPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = false;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the driver's extensions");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = true;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
|
||||
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
|
||||
// would reject.
|
||||
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
|
||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
||||
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
||||
|
||||
@@ -78,11 +78,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -102,9 +102,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// What an indexed draw has to do about primitive restart before it can be issued.
|
||||
//
|
||||
// Desktop GL restarts on an application-chosen index (glPrimitiveRestartIndex under
|
||||
// GL_PRIMITIVE_RESTART); GLES core restarts only on the all-ones value of the index type
|
||||
// (GL_PRIMITIVE_RESTART_FIXED_INDEX), which the render-state push enables for BOTH caps.
|
||||
// That leaves three cases, and the difference between the last two is not cosmetic - one
|
||||
// adds restarts, the other has to take away restarts the driver would otherwise make.
|
||||
enum class RestartSubstitutionKind : Uint8 {
|
||||
// Nothing to do: restart is off, the fixed-index cap is on, or the application's
|
||||
// restart index already IS the type's all-ones value. The overwhelmingly common answer.
|
||||
None,
|
||||
// The application's index is representable in this index type and differs from the
|
||||
// all-ones value: the index DATA has to be rewritten so the driver restarts where the
|
||||
// application asked.
|
||||
RewriteIndices,
|
||||
// The application's index cannot be held by this index type at all. GL 4.6 core 10.3.6
|
||||
// compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX, so no index can match and the draw restarts NOWHERE - but the
|
||||
// render-state push has already enabled the driver's fixed-index restart, so the
|
||||
// all-ones value has to be un-restarted for the duration of the draw.
|
||||
SuppressRestart,
|
||||
};
|
||||
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType);
|
||||
|
||||
// Turns the driver's fixed-index restart off for one draw and back on afterwards, for the
|
||||
// SuppressRestart case above. Separate from the substitution below because the multi-draw
|
||||
// tiers need it on its own: they rewrite the index stream themselves and only ever need the
|
||||
// cap half. Inert for every other kind, and it never touches the render-state shadow - it
|
||||
// puts the driver back exactly where SyncRenderState left it.
|
||||
class ScopedSuppressedPrimitiveRestart {
|
||||
public:
|
||||
explicit ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind);
|
||||
~ScopedSuppressedPrimitiveRestart();
|
||||
ScopedSuppressedPrimitiveRestart(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
ScopedSuppressedPrimitiveRestart& operator=(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
|
||||
private:
|
||||
Bool m_suppressed = false;
|
||||
};
|
||||
|
||||
// Swaps in a scratch element array buffer holding a copy of the index data in which the
|
||||
// application's restart index has been replaced by the value GLES restarts on. Inert
|
||||
// (and free) unless ResolveRestartSubstitution asks for it. The swap lives for the
|
||||
// object's lifetime, so it covers every pass of a viewport-routed draw, and the previous
|
||||
// GL_ELEMENT_ARRAY_BUFFER name is restored on destruction - which matters beyond tidiness,
|
||||
// because the VAO twin memoises that it already synced that binding.
|
||||
//
|
||||
// The copy may be WIDER than the source (see IndexType): when the source already contains
|
||||
// the type's all-ones value as an ordinary vertex index, that value cannot double as the
|
||||
// restart sentinel, and widening is the only way to keep both meanings. Callers must
|
||||
// therefore take the index type from this object, not from their own argument.
|
||||
class ScopedRestartIndexSubstitution {
|
||||
public:
|
||||
// count/indices describe the draw's index range when the CPU knows it. Pass
|
||||
// count == 0 for an indirect draw, whose count lives in GPU memory: the whole bound
|
||||
// element array buffer is rewritten instead, so every element keeps its position and
|
||||
// a GPU-resident firstIndex - an ELEMENT index, so it survives widening too - still
|
||||
// addresses the index it named.
|
||||
ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices);
|
||||
~ScopedRestartIndexSubstitution();
|
||||
ScopedRestartIndexSubstitution(const ScopedRestartIndexSubstitution&) = delete;
|
||||
ScopedRestartIndexSubstitution& operator=(const ScopedRestartIndexSubstitution&) = delete;
|
||||
|
||||
// False only when a substitution was needed and could not be made. The draw must
|
||||
// then be skipped: issuing it would let the driver silently drop every restart and
|
||||
// weld the primitives on either side together, which is worse than drawing nothing.
|
||||
Bool DrawIsValid() const { return m_valid; }
|
||||
// The element-array offset (or client pointer) the draw must use. Identical to what
|
||||
// was passed in unless a substitution was made.
|
||||
const void* Indices() const { return m_indices; }
|
||||
// The index type the draw must be issued with. Identical to the constructor's unless
|
||||
// the copy had to be widened to keep an all-ones vertex index distinguishable from the
|
||||
// restart sentinel.
|
||||
GLenum IndexType() const { return m_indexType; }
|
||||
|
||||
private:
|
||||
// Declared before m_capOverride so it is initialised first (members initialise in
|
||||
// declaration order): the whole decision is made once, and both the cap override and the
|
||||
// constructor body read the same answer.
|
||||
RestartSubstitutionKind m_kind = RestartSubstitutionKind::None;
|
||||
ScopedSuppressedPrimitiveRestart m_capOverride;
|
||||
const void* m_indices = nullptr;
|
||||
GLenum m_indexType = 0;
|
||||
Uint m_previousBinding = 0;
|
||||
Bool m_substituted = false;
|
||||
Bool m_valid = true;
|
||||
};
|
||||
|
||||
// Drops the scratch element array buffer the substitution above stages through. Like
|
||||
// MultiDrawImpl's scratch names it is abandoned rather than deleted: the name belongs to
|
||||
// the dead ES context, and deleting it would target whatever its successor handed out.
|
||||
void OnRestartSubstitutionContextDestroyed();
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
@@ -141,7 +230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
||||
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
||||
//
|
||||
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// Whether emulation runs. Off only under MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// restores the pre-emulation path as a negative control.
|
||||
Bool ViewportArrayEmulationEnabled();
|
||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||
@@ -372,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the owning thread replaying them: guard both fields with pendingMutex.
|
||||
Bool pendingRespecify = false;
|
||||
VecRange1D pendingRanges;
|
||||
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
|
||||
// stage + glCopyBufferSubData at the next sync; see
|
||||
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
|
||||
// the coherent mapping - an in-place host write tears the in-flight frames
|
||||
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
|
||||
struct PendingResidentWrite {
|
||||
SizeT offset = 0;
|
||||
Vector<Uint8> bytes;
|
||||
};
|
||||
Vector<PendingResidentWrite> pendingResidentWrites;
|
||||
std::mutex pendingMutex;
|
||||
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||
@@ -455,12 +554,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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/
|
||||
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
|
||||
// skips the GL call when the (id, range) already at that index matches, like the
|
||||
// array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
|
||||
// (context may reset).
|
||||
// Binds the transform feedback capture points [0, bufferCount) from the frontend
|
||||
// state, and touches nothing else - in particular it never binds a zero the
|
||||
// application did not ask for. See the definition for why that matters on Mali.
|
||||
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
|
||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||
void InvalidateIndexedBufferBindingCache();
|
||||
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
|
||||
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
|
||||
// calls this on each bind/delete.
|
||||
void InvalidateTransformFeedbackBindingShadows();
|
||||
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
||||
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
||||
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
||||
@@ -510,6 +618,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Present()-time upkeep: records the frame's high-water mark for reclamation
|
||||
// and deletes grown-away ring stores once the GPU is done with them.
|
||||
void UboRingOnPresent();
|
||||
|
||||
// --- Texture unpack-PBO ring ----------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging TEXTURE UPLOADS. A
|
||||
// glTexSubImage from client memory hands the driver a pointer it must read
|
||||
// before the call returns, so the copy has to be ordered against whatever GPU
|
||||
// work still reads the destination texture: Mali resolves that by BLOCKING the
|
||||
// calling thread (osup_sync_object_wait) instead of ghosting, and Minecraft
|
||||
// re-uploads animated atlas sprites and the lightmap every tick into textures
|
||||
// the in-flight frame is still sampling. Staging the bytes into a
|
||||
// GPU-visible unpack PBO and passing an OFFSET instead lets the driver queue
|
||||
// the copy in the command stream with no CPU wait at all.
|
||||
//
|
||||
// Same reclamation contract as the UBO ring: no ring bytes are recycled before
|
||||
// the frame that referenced them completed on the GPU, so a staged block stays
|
||||
// intact for as long as the queued transfer can still be reading it. The store
|
||||
// therefore settles at roughly (bytes staged per frame) x (frames in flight),
|
||||
// which is what to watch if this ring ever shows up in an RSS regression: it
|
||||
// grows on demand from 4 MiB and is capped, not unbounded.
|
||||
//
|
||||
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
|
||||
// EXT_buffer_storage / fences are missing, the ES context is not current, or
|
||||
// ring creation already failed under this context. Callers then upload from
|
||||
// the client pointer exactly as before.
|
||||
Bool UnpackRingAvailable();
|
||||
// Bump-allocate `size` bytes aligned to 64 (a PBO-sourced glTexSubImage only
|
||||
// owes the driver the pixel type's own alignment). Grows the ring when the
|
||||
// in-flight span would be overrun; false when the request exceeds the ring's
|
||||
// size cap or storage (re)creation fails.
|
||||
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset);
|
||||
void* UnpackRingMappedPtr();
|
||||
Uint UnpackRingBufferId();
|
||||
// Largest single staging request the ring can ever satisfy.
|
||||
SizeT UnpackRingMaxBytes();
|
||||
void UnpackRingOnPresent();
|
||||
|
||||
// --- Buffer upload ring ---------------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
|
||||
// (glBufferSubData / non-persistent map flushes) whose destination store may
|
||||
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
|
||||
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
|
||||
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
|
||||
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
|
||||
// each such call serialized against the whole GPU queue (~1 fps while chunks
|
||||
// stream in, and again on every camera pan). App SubData ranges are queued on
|
||||
// the resource instead (the frontend shadow already holds the bytes) and
|
||||
// draw-time sync drains them: bytes staged into this ring, then one
|
||||
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
|
||||
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
|
||||
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
|
||||
// historical immediate-upload path (negative control / escape hatch).
|
||||
void UploadRingOnPresent();
|
||||
} // namespace BufferImpl
|
||||
|
||||
namespace VertexArrayImpl {
|
||||
@@ -792,6 +951,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||
@@ -824,6 +995,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// `contextId`/`samplingGeneration` are the frontend context's current
|
||||
// values, hoisted by the caller so a per-draw list walk reads them once
|
||||
// instead of per texture. `t` must be the live frontend texture.
|
||||
// True while a driver-side re-mint has left the parameter caches describing a texture
|
||||
// that no longer exists; SyncTextureObjectToBackend re-pushes them in the same sync.
|
||||
Bool NeedsParameterResync() const { return m_forceTextureParamsResync || m_forceSamplerResync; }
|
||||
|
||||
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||
Uint64 samplingGeneration) const {
|
||||
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||
@@ -867,6 +1042,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
||||
// the split the shader was rewritten against. 0 when this texture takes no split.
|
||||
Uint m_bufferImageSplitViewId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
Uint m_viewSourceBackendTextureId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
@@ -900,7 +1086,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint16 m_syncedShapeParamsVersion = 0;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
UintVec2 m_cacheLodRange = {0, 1000};
|
||||
// All three representations plus the form, because none of them alone identifies the
|
||||
// border colour the driver texture is holding: two integer borders can share one float
|
||||
// (anything differing above 2^24), and a Float -> Int transition can leave every number
|
||||
// unchanged while still needing a different driver entry point.
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 m_cacheBorderColorI = {0, 0, 0, 0};
|
||||
UintVec4 m_cacheBorderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm m_cacheBorderColorForm = BorderColorForm::Float;
|
||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||
@@ -915,6 +1108,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||
// once, or an unchanged version would skip the re-push forever.
|
||||
Bool m_forceTextureParamsResync = false;
|
||||
// The same problem for the FILTER state, which lives in m_cacheSamplerParameters and
|
||||
// is gated on the frontend sampler's version rather than on the params version. A
|
||||
// re-mint leaves that cache describing values the new driver texture never received,
|
||||
// and an unchanged sampler version would then skip re-pushing them forever. This
|
||||
// matters more than mis-filtering: ES makes a texture INCOMPLETE when its filters do
|
||||
// not suit its level set (any integer texture with a non-NEAREST filter, or a
|
||||
// single-level texture with a mipmapping filter), and an incomplete texture samples
|
||||
// (0, 0, 0, 1) rather than its contents.
|
||||
Bool m_forceSamplerResync = false;
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
@@ -1377,6 +1579,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL the same synthesized stage was built with, for
|
||||
// the same reason: ES has neither the state nor an entry point to forward it to, so
|
||||
// glPatchParameterfv's values are compiled in as literals and a program built with one
|
||||
// set is stale for another. Meaningless (and never read) when the patch-vertices field
|
||||
// above is -1, which is the gate the draw path tests first.
|
||||
const FloatVec4& GetPassthroughTessControlOuterLevel() const {
|
||||
return m_passthroughTessControlOuterLevel;
|
||||
}
|
||||
const FloatVec2& GetPassthroughTessControlInnerLevel() const {
|
||||
return m_passthroughTessControlInnerLevel;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1447,6 +1660,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
|
||||
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||
String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames,
|
||||
@@ -1477,6 +1691,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
// The default tessellation levels baked into that same stage. Only meaningful while
|
||||
// the field above is not -1.
|
||||
FloatVec4 m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||
|
||||
@@ -29,16 +29,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The all-ones value of an index type, which is what GL restarts on once
|
||||
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||
// restart always restarts here and nowhere else.
|
||||
// The index value this batch restarts on, compared at 32 bits against the zero-extended
|
||||
// source index. Normally the all-ones value of the source type, which is what
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
|
||||
// GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
|
||||
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
|
||||
// driver restarts on.
|
||||
//
|
||||
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
|
||||
// restart index the source type cannot hold simply matches nothing, so returning it
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
default: return 0xFFFFFFFFu;
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
@@ -275,10 +280,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer) {
|
||||
Bool hasIndexBuffer, Bool arbitraryRestart) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
|
||||
// only ever restarts on the all-ones value. Every tier but the rebased one hands
|
||||
// the application's own index data to the driver, which would then see no restarts
|
||||
// at all and weld the primitives together. The rebased tier is the one that
|
||||
// REWRITES the stream, and RestartSentinelFor already tells it which value to
|
||||
// translate, so it is the only tier this batch can take.
|
||||
if (arbitraryRestart) {
|
||||
return GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
@@ -488,6 +503,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
|
||||
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
|
||||
// a real vertex index while restarting on a different one is the one shape 32 bits
|
||||
// cannot express - the same corner the single-draw substitution reports.
|
||||
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
|
||||
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
|
||||
"has no wider sentinel to move to.",
|
||||
restartSentinel);
|
||||
}
|
||||
g_indexStaging.resize(total);
|
||||
SizeT cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -852,8 +877,14 @@ void main() {
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (drawcount <= 0 || !count || !indices) return;
|
||||
// State-independent and possibly throwing, so it runs before any GL work.
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
// Read before any GL work, because it decides the tier below: a desktop restart index
|
||||
// the driver does not know about can only be honoured by the tier that rewrites the
|
||||
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
|
||||
// needs no rewrite at all - nothing can match it - but it does need the driver's own
|
||||
// fixed-index restart held off for the batch, which is what the scope below does.
|
||||
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
|
||||
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
|
||||
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
|
||||
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
@@ -889,7 +920,8 @@ void main() {
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||
const GLESMultiDrawMode tier =
|
||||
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -921,8 +953,10 @@ void main() {
|
||||
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) {
|
||||
// entry points can receive - except that the base-vertex replay hands the
|
||||
// application's own indices to the driver, which cannot restart on a desktop
|
||||
// restart index, so that batch has only the rewriting floor.
|
||||
if (!drawn && !arbitraryRestart) {
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
#include "Managers.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
@@ -26,6 +28,7 @@
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -124,6 +127,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
}
|
||||
// Outside the caveat branch on purpose: the driver CAN create the native narrow
|
||||
// storage - the capability probes say so - it just cannot be trusted as a raw-copy
|
||||
// endpoint. Texture and renderbuffer targets both come through here, which is what
|
||||
// keeps a renderbuffer -> texture copy of these formats same-ES-format when the
|
||||
// widening engages.
|
||||
if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) {
|
||||
options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm;
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
} // namespace
|
||||
@@ -181,6 +192,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return options;
|
||||
}
|
||||
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) {
|
||||
switch (internalFormat) {
|
||||
// TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG
|
||||
// converter folds the two spellings onto one logical format.
|
||||
case TextureInternalFormat::RGB5:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (MG_Config::Features.EsprytWidenPacked16Storage) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
return true;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
return false;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
// Behind the backend gate on purpose: the memoized probe latches its first answer
|
||||
// for the whole process, and before the backend is up the GL function table may
|
||||
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
|
||||
// forever. Once the backend exists, the first narrow-format image this process
|
||||
// creates runs the probe on a live context.
|
||||
if (pActiveBackendObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
|
||||
}
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -712,6 +753,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation) {
|
||||
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
||||
switch (tier) {
|
||||
case Tier::ExtensionEXT:
|
||||
return tessellation ? "GL_EXT_tessellation_point_size" : "GL_EXT_geometry_point_size";
|
||||
case Tier::ExtensionOES:
|
||||
return tessellation ? "GL_OES_tessellation_point_size" : "GL_OES_geometry_point_size";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The gl_ViewportIndex story, one built-in over: ESSL 320 makes the tessellation and
|
||||
// geometry STAGES core but leaves gl_PointSize out of their gl_PerVertex entirely,
|
||||
// and SPIRV-Cross - which only ever sees a SPIR-V BuiltIn PointSize decoration -
|
||||
// prints the identifier with no directive behind it. Same hard rule as the two
|
||||
// neighbours: never emitted speculatively, because `#extension` on a name the driver
|
||||
// does not advertise is a compile error of its own.
|
||||
if (extensionName == nullptr || glslCode.find(extensionName) != String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
const String directive = String("#extension ") + extensionName + " : require\n";
|
||||
// Right after the #version line, the one position that must stay first;
|
||||
// ForceSupporterOutput's scan for the LAST #extension directive still finds
|
||||
// whichever one that ends up being.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return directive + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + directive;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, directive);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BakeImageFormatQualifiers(String glslCode,
|
||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -836,7 +918,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -866,12 +950,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
for (Uint i = 0; i < 4; ++i) {
|
||||
source += " gl_TessLevelOuter[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
|
||||
}
|
||||
for (Uint i = 0; i < 2; ++i) {
|
||||
source += " gl_TessLevelInner[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
|
||||
}
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||
|
||||
// Whether this format's ES storage is widened to 8-bit-per-channel because the
|
||||
// driver stores some packed16 allocations with a mirrored field order
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
|
||||
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
|
||||
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
|
||||
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
|
||||
// is no longer 16-bit packed.
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
@@ -273,6 +282,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// error, so this is never emitted speculatively. A no-op when not needed or already
|
||||
// present.
|
||||
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
||||
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
|
||||
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
|
||||
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
|
||||
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
|
||||
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
|
||||
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
|
||||
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
|
||||
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
|
||||
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
|
||||
// advertises neither spelling, because requesting an unadvertised extension is itself a
|
||||
// compile error. A no-op when nullptr or already present.
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName);
|
||||
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
|
||||
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
|
||||
// ones; the two extensions are separate and neither implies the other.
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
@@ -368,11 +393,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
|
||||
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
|
||||
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
|
||||
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
|
||||
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
@@ -382,7 +407,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
@@ -505,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
|
||||
@@ -500,11 +500,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
@@ -512,9 +512,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
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_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,
|
||||
@@ -533,6 +540,79 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
|
||||
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
|
||||
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
|
||||
// every immutable-storage texture (see the comment there).
|
||||
E_GL_ARB_texture_view,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
|
||||
// now reaches the shader on BOTH backends - Magma resolves the lowered
|
||||
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
|
||||
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
|
||||
// landed, because the counter silently read whatever was bound as SSBO N instead.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it. Kept identical to the DirectGLES block so the two
|
||||
// backends do not disagree about what MobileGL is.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
@@ -544,7 +624,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||
@@ -585,6 +665,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
|
||||
// view type cannot be created without the imageCubeArray device feature - so the string
|
||||
// follows the feature, not the version, exactly as the per-layer attachment bit does.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -715,7 +807,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
|
||||
m_vulkanCaps.SupportsImageCubeArray);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -763,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
@@ -912,6 +1006,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it the limit describes a capacity no shader may use, so report none.
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||
// The cull pair, gated on its own feature. shaderCullDistance is separate from
|
||||
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
|
||||
// gate rather than riding on the clip one.
|
||||
m_dynamicParameters.MaxCullDistances =
|
||||
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
|
||||
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
|
||||
// with only one of the two features must not report a combined capacity that implies the
|
||||
// other, so the gate is "either feature" and the value never drops below what is enabled.
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
@@ -974,6 +1081,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||
// detected answer's declines.
|
||||
{
|
||||
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
supportsStagePointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the device feature");
|
||||
supportsStagePointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||
}
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
|
||||
@@ -70,12 +70,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// 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 nonZeroIndirectBaseInstanceSupported);
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -1206,6 +1206,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
|
||||
// draws, where the renderer's reroute is armed (the affected driver's
|
||||
// stream query counts nothing without an open capture; see
|
||||
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
|
||||
// slots above, which keep the span's XFB-active draws.
|
||||
Vector<Uint32> rerouteSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1215,11 +1221,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale queries are always safe to delete.
|
||||
Uint64 rendererGeneration = 0;
|
||||
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
||||
// capture saw, so a draw made while the span was paused is invisible to it -
|
||||
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
||||
// emitted regardless. The delta closes that gap at result time.
|
||||
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
|
||||
// counts only what the capture saw, so a draw made while the span was paused is
|
||||
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
|
||||
// processing stage emitted regardless. The delta closes that gap at result time.
|
||||
Uint64 pausedPrimitiveSnapshot = 0;
|
||||
// ...unless the GPU already counted those paused draws when the span opened -
|
||||
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
|
||||
// draw with no open capture, paused ones included) or, where the probe measured
|
||||
// the stream query as counting capture-less draws, through the stream slot the
|
||||
// paused draw still takes. Adding the CPU delta on top would count them twice,
|
||||
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
|
||||
// points write it and it answers 0 for GL_PATCHES.
|
||||
Bool pausedPrimitivesCountedByGpu = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1313,12 +1327,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
|
||||
!query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) {
|
||||
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
query->pausedPrimitiveSnapshot;
|
||||
}
|
||||
@@ -1367,6 +1382,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
query->pausedPrimitiveSnapshot =
|
||||
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
|
||||
// disarms: the answer is then what this span will actually do for every draw.
|
||||
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -1377,7 +1395,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
|
||||
query->rerouteSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
|
||||
@@ -201,11 +201,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
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.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
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)));
|
||||
@@ -435,6 +440,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -37,11 +37,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
|
||||
// identity and both are hashed. The renderer leaves the enable false unless the
|
||||
// device's sampleRateShading feature was enabled
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
|
||||
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
|
||||
// the levels glPatchParameterfv set are compiled INTO that module and are not a
|
||||
// function of the program or of patchControlPoints - see the note on
|
||||
// passthroughTessControlStage.
|
||||
Uint64 passthroughTessControlKey = 0;
|
||||
// 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
|
||||
@@ -87,8 +115,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
|
||||
// of the default tessellation levels - the first two of which ComputeHash already
|
||||
// mixes in, and the third of which arrives through passthroughTessControlKey above.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
// in the order glslang declares them - which is the order a redeclaration must use.
|
||||
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
|
||||
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
|
||||
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
|
||||
enum class PerVertexMemberBit : Uint32 {
|
||||
Position = 1u << 0,
|
||||
PointSize = 1u << 1,
|
||||
ClipDistance = 1u << 2,
|
||||
CullDistance = 1u << 3,
|
||||
};
|
||||
// What a program parsed below #version 450 carries, and the fallback when a module's
|
||||
// block cannot be read.
|
||||
static constexpr Uint32 kDefaultPerVertexMembers =
|
||||
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
|
||||
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
@@ -185,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
// The program has a tessellation or geometry module declaring TessellationPointSize /
|
||||
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
|
||||
// is off, so a pipeline built from it is invalid usage
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
|
||||
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
|
||||
// implements one level up, and the counterpart of the DirectGLES arm that reports a
|
||||
// driver with neither point-size extension by name.
|
||||
//
|
||||
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
@@ -194,6 +237,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
|
||||
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
|
||||
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
|
||||
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
|
||||
// which the source processor rewrites to "#version 460 core" - carries four members
|
||||
// where a 430 program carries three. A fixed three-member pass-through fed the
|
||||
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
|
||||
// this whole family is written around.
|
||||
Uint32 passthroughPerVertexMembers = 0;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -249,6 +301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -267,6 +320,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -311,6 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -329,6 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -396,12 +452,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool tessellationAndGeometryPointSizeEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
@@ -448,6 +506,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
|
||||
// sampler form above deliberately does not answer. Kept separate rather than folded in
|
||||
// because the two are asked in different places for different reasons: a sampler's domain
|
||||
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
|
||||
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
|
||||
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
|
||||
// format at all, and the numeric domain is then the only thing that constrains it.
|
||||
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
@@ -477,18 +543,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
|
||||
// member set, because all three decide what the generator emits. The size comes from
|
||||
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
|
||||
// - draw state rather than link state, and the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
|
||||
// own SPIR-V, so two programs at different GLSL versions need different modules. The
|
||||
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
|
||||
// part of that pipeline's identity. Compiling is bounded by the number of distinct
|
||||
// (size, levels, members) combinations a program draws with - one or two in practice -
|
||||
// and only ever happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The identity of one such module: everything the generator bakes in, folded into a
|
||||
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
|
||||
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
|
||||
// same value into the pipeline hash so a pipeline can never be handed a module built for
|
||||
// different levels or a different block shape.
|
||||
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
|
||||
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
|
||||
// Input variable that is an array of a Block-decorated struct). Zero when the module has
|
||||
// no such block. Exposed for tests, which is the only way to pin the shape agreement
|
||||
// without a device.
|
||||
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -531,6 +619,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
|
||||
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
|
||||
// GeometryPointSize is refused at build time (see VkProgramObject::
|
||||
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
@@ -548,11 +641,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// Pass-through tessellation control stages by the identity of what was compiled into
|
||||
// them - the input patch size and the six default tessellation levels, folded into one
|
||||
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
|
||||
// cannot simply be keyed on the patch size any more). A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
//
|
||||
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
|
||||
// nothing, so an application that recomputes a level per frame mints a new key per frame.
|
||||
// Reaching the cap destroys every module and starts over (see the flush in
|
||||
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
|
||||
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -11,13 +11,19 @@
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject1D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2DCube.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject3D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObjectStubs.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -27,6 +33,154 @@
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u;
|
||||
// One id for every storage-image placeholder. They are never reachable through GL - no
|
||||
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
|
||||
// id only has to stay clear of the application's, exactly like the sampled fallback's.
|
||||
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
|
||||
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
|
||||
// single-sampled fallback they cannot be reinterpreted into another domain at view time
|
||||
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
|
||||
// same reason as the two above - they must not collide with anything glGenTextures can
|
||||
// hand out.
|
||||
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
|
||||
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
|
||||
|
||||
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
|
||||
// do reaches one, so anything keyed on the GL object an application bound - image-unit
|
||||
// aliasing above all - has to leave them alone.
|
||||
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) return false;
|
||||
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
|
||||
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
|
||||
(index >= kFallbackMultisampleExternalIndexBase &&
|
||||
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
|
||||
}
|
||||
|
||||
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support
|
||||
// format for uniform texel buffers, storage texel buffers and storage images alike
|
||||
// (Vulkan 1.0, "Required Format Support"), which is what makes them a fallback that
|
||||
// cannot itself fail for want of device features.
|
||||
VkFormat PlaceholderFormatForNumericDomain(SamplerNumericDomain numericDomain) {
|
||||
switch (numericDomain) {
|
||||
case SamplerNumericDomain::Float:
|
||||
return VK_FORMAT_R32_SFLOAT;
|
||||
case SamplerNumericDomain::SignedInteger:
|
||||
return VK_FORMAT_R32_SINT;
|
||||
case SamplerNumericDomain::UnsignedInteger:
|
||||
return VK_FORMAT_R32_UINT;
|
||||
case SamplerNumericDomain::Unknown:
|
||||
break;
|
||||
}
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
Bool BufferFormatSupportsFeature(VkPhysicalDevice physicalDevice, VkFormat format,
|
||||
VkFormatFeatureFlags requiredFeature) {
|
||||
if (physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & requiredFeature) == requiredFeature;
|
||||
}
|
||||
|
||||
// Reverse of MG_Util::ConvertTextureInternalFormatToVkEnum. A placeholder texture is
|
||||
// built through the ordinary frontend texture object (that is what gets it an image with
|
||||
// STORAGE usage, a GENERAL transition and a view, for free), and that object is described
|
||||
// by a GL internal format - while everything upstream of here speaks VkFormat. Scanned
|
||||
// rather than tabulated: it runs once per (target, format) placeholder ever created, the
|
||||
// enum is ~70 entries, and a second hand-written table is a second thing to drift.
|
||||
// Ascending order matters: the sized formats precede the unsized aliases, so a scan
|
||||
// answers with the sized one.
|
||||
TextureInternalFormat InternalFormatForVkFormat(VkFormat format) {
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
return TextureInternalFormat::Unknown;
|
||||
}
|
||||
for (Int index = 0; index < static_cast<Int>(TextureInternalFormat::TextureInternalFormatCount);
|
||||
++index) {
|
||||
const auto candidate = static_cast<TextureInternalFormat>(index);
|
||||
if (MG_Util::ConvertTextureInternalFormatToVkEnum(candidate) == format) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return TextureInternalFormat::Unknown;
|
||||
}
|
||||
|
||||
// What a 1x1 placeholder of a given target has to allocate for the backend to give it the
|
||||
// Vulkan view type that target's image declaration demands (see
|
||||
// VkTextureManager's TryResolveTextureShapeInfo, which reads exactly these two things).
|
||||
struct PlaceholderShape {
|
||||
Array<TextureUploadTarget, 6> uploadTargets{};
|
||||
Uint32 uploadTargetCount = 0;
|
||||
// The GL depth of the single level: the array length for an array target, the depth
|
||||
// for a 3D one, and 6 for a cube map array (one whole cube).
|
||||
Int depth = 1;
|
||||
Bool valid = false;
|
||||
};
|
||||
|
||||
PlaceholderShape PlaceholderShapeForTarget(TextureTarget target) {
|
||||
PlaceholderShape shape{};
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
shape = {{TextureUploadTarget::Texture1D}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::Texture2D:
|
||||
shape = {{TextureUploadTarget::Texture2D}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::TextureRectangle:
|
||||
shape = {{TextureUploadTarget::TextureRectangle}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
shape = {{TextureUploadTarget::Texture3D}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::Texture1DArray:
|
||||
shape = {{TextureUploadTarget::Texture1DArray}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::Texture2DArray:
|
||||
shape = {{TextureUploadTarget::Texture2DArray}, 1, 1, true};
|
||||
break;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
shape = {{TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
|
||||
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
|
||||
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ},
|
||||
6, 1, true};
|
||||
break;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
// Layers are cube faces, so the count must be a whole number of cubes.
|
||||
shape = {{TextureUploadTarget::CubeMapArray}, 1, 6, true};
|
||||
break;
|
||||
default:
|
||||
// Multisample targets above all: their descriptor needs a multisample view.
|
||||
break;
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
// TextureObjectMipmap, not ITextureObject: AllocateStorage and MarkStorageDirty live
|
||||
// there, and every placeholder shape above is one of its subclasses.
|
||||
SharedPtr<MG_State::GLState::TextureObjectMipmap> MakePlaceholderTextureObject(TextureTarget target,
|
||||
Uint index) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return MakeShared<MG_State::GLState::TextureObject1D>(index);
|
||||
case TextureTarget::Texture2D:
|
||||
return MakeShared<MG_State::GLState::TextureObject2D>(index);
|
||||
case TextureTarget::TextureRectangle:
|
||||
return MakeShared<MG_State::GLState::TextureObjectRectangle>(index);
|
||||
case TextureTarget::Texture3D:
|
||||
return MakeShared<MG_State::GLState::TextureObject3D>(index);
|
||||
case TextureTarget::Texture1DArray:
|
||||
return MakeShared<MG_State::GLState::TextureObject1DArray>(index);
|
||||
case TextureTarget::Texture2DArray:
|
||||
return MakeShared<MG_State::GLState::TextureObject2DArray>(index);
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return MakeShared<MG_State::GLState::TextureObject2DCube>(index);
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return MakeShared<MG_State::GLState::TextureObjectCubeMapArray>(index);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Bool FindFramebufferAttachmentForTexture(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
@@ -47,7 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto attachedTexture = attachment.GetTexture();
|
||||
if (attachedTexture && attachedTexture.get() == &texture) {
|
||||
outAttachment = attachmentType;
|
||||
outLevel = attachment.GetTextureLevel();
|
||||
outLevel = static_cast<Int>(ToStorageMipLevel(attachment.GetTexture().get(),
|
||||
attachment.GetTextureLevel()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -113,7 +268,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
|
||||
}
|
||||
|
||||
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
Bool UniformManager::Initialize(VkDevice device, VkPhysicalDevice physicalDevice,
|
||||
VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings, Uint32 setsPerFrame,
|
||||
@@ -121,6 +277,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Shutdown();
|
||||
|
||||
MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "UniformDescriptorBinder::Initialize requires valid VkDevice");
|
||||
MOBILEGL_ASSERT(physicalDevice != VK_NULL_HANDLE,
|
||||
"UniformDescriptorBinder::Initialize requires valid VkPhysicalDevice");
|
||||
MOBILEGL_ASSERT(bufferManager != nullptr, "UniformDescriptorBinder::Initialize requires valid buffer manager");
|
||||
MOBILEGL_ASSERT(programFactory != nullptr,
|
||||
"UniformDescriptorBinder::Initialize requires valid program factory");
|
||||
@@ -133,6 +291,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"UniformDescriptorBinder::Initialize requires valid sampler manager");
|
||||
|
||||
m_device = device;
|
||||
m_physicalDevice = physicalDevice;
|
||||
m_bufferManager = bufferManager;
|
||||
m_programFactory = programFactory;
|
||||
m_minDynamicOffsetAlignment = std::max<VkDeviceSize>(1, minUniformBufferOffsetAlignment);
|
||||
@@ -171,6 +330,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void UniformManager::Shutdown() {
|
||||
// Before the per-frame loop, because these views are NOT owned by any frame slot (see
|
||||
// m_unboundTexelBufferViews) and the loop below is what clears m_device.
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
for (const auto& viewEntry : m_unboundTexelBufferViews) {
|
||||
if (viewEntry.second != VK_NULL_HANDLE) {
|
||||
vkDestroyBufferView(m_device, viewEntry.second, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_unboundTexelBufferViews.clear();
|
||||
m_unboundStorageImageTextures.clear();
|
||||
for (auto& frame : m_frames) {
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
for (auto& view : frame.texelBufferViews) {
|
||||
@@ -197,6 +367,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_bufferManager = nullptr;
|
||||
m_programFactory = nullptr;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_physicalDevice = VK_NULL_HANDLE;
|
||||
m_minDynamicOffsetAlignment = 1;
|
||||
m_frameCount = 0;
|
||||
m_maxBindings = 0;
|
||||
@@ -207,6 +378,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_textureManager = nullptr;
|
||||
m_samplerManager = nullptr;
|
||||
m_fallbackTexture2D.reset();
|
||||
m_fallbackMultisampleTextures.clear();
|
||||
}
|
||||
|
||||
void UniformManager::BeginFrame(Uint32 frameIndex) {
|
||||
@@ -343,7 +515,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
// The binding's sampler class, read here rather than through the `numericDomain`
|
||||
// local further down (it is declared after this point): the multisample placeholder
|
||||
// has to be built in the class the shader will read it in.
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
|
||||
texture = fallbackHolder.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
@@ -416,16 +591,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
||||
SamplerResolveMemo* viewFormatMemo =
|
||||
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
|
||||
// The format this GL texture presents to the shader. For a texture created by
|
||||
// glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core
|
||||
// 8.18), not the storage image's own - resolving the numeric domain against the latter
|
||||
// would pick a sampled view for a format the shader never declared. The probe is behind
|
||||
// IsTextureView() so nothing about the ordinary per-draw path changes.
|
||||
const VkFormat sampledSourceFormat =
|
||||
texture->IsTextureView()
|
||||
? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format
|
||||
: resource->format;
|
||||
VkFormat sampledViewFormat;
|
||||
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
|
||||
viewFormatMemo->viewFormatSource == resource->format &&
|
||||
viewFormatMemo->viewFormatSource == sampledSourceFormat &&
|
||||
viewFormatMemo->viewFormatDomain == numericDomain) {
|
||||
sampledViewFormat = viewFormatMemo->viewFormat;
|
||||
} else {
|
||||
sampledViewFormat =
|
||||
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
|
||||
VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain);
|
||||
if (viewFormatMemo != nullptr) {
|
||||
viewFormatMemo->viewFormatSource = resource->format;
|
||||
viewFormatMemo->viewFormatSource = sampledSourceFormat;
|
||||
viewFormatMemo->viewFormatDomain = numericDomain;
|
||||
viewFormatMemo->viewFormat = sampledViewFormat;
|
||||
viewFormatMemo->viewFormatValid = true;
|
||||
@@ -440,9 +624,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
|
||||
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
|
||||
// already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL
|
||||
// texture view is excluded because resource->sampledView belongs to the texture it VIEWS
|
||||
// - same image, but the storage texture's level range and depth/stencil aspect, which is
|
||||
// exactly the state a view exists to differ on.
|
||||
const VkImageView sampledImageView =
|
||||
sampledViewFormat == resource->format
|
||||
(!texture->IsTextureView() && sampledViewFormat == resource->format)
|
||||
? resource->sampledView
|
||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||
if (sampledImageView == VK_NULL_HANDLE) {
|
||||
@@ -547,7 +734,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->sampledLevelCount),
|
||||
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
||||
samplerBindingOverride.imageView :
|
||||
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
||||
// Same reason as in ResolveSamplerDescriptor: the resource's own views describe
|
||||
// the storage texture, so a view has to be asked for its own.
|
||||
(samplerBindingOverride.texture->IsTextureView()
|
||||
? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture,
|
||||
VK_FORMAT_UNDEFINED)
|
||||
: (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)),
|
||||
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
||||
samplerBindingOverride.imageLayout : resource->layout,
|
||||
};
|
||||
@@ -674,11 +866,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range");
|
||||
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||
"ResolveTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
||||
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
||||
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
// NOT an error, and not a reason to lose the draw. A texture unit with nothing on it
|
||||
// is a legal GL state (4.6 core 8.24): the sampler is incomplete, so a fetch through
|
||||
// it returns undefined values - the same answer the sampled path above gives with its
|
||||
// fallback texture, which a buffer texture simply cannot use because its descriptor is
|
||||
// a VkBufferView. A per-format placeholder view is the equivalent for this kind.
|
||||
const VkBufferView placeholder =
|
||||
AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false);
|
||||
if (placeholder == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound, and the "
|
||||
"placeholder descriptor could not be created", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') is unbound; using the placeholder descriptor",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
outBufferView = placeholder;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
@@ -693,9 +903,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
// A buffer texture with no buffer object attached is INCOMPLETE, not illegal (GL 4.6
|
||||
// core 8.9), and sampling an incomplete texture is undefined - so this too keeps the
|
||||
// draw on a placeholder rather than dropping it.
|
||||
const VkBufferView placeholder =
|
||||
AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false);
|
||||
if (placeholder == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound, "
|
||||
"and the placeholder descriptor could not be created", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') has no attached GL buffer; using the "
|
||||
"placeholder descriptor", binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
outBufferView = placeholder;
|
||||
return true;
|
||||
}
|
||||
|
||||
BufferSlice slice{};
|
||||
@@ -785,12 +1007,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
|
||||
// and stores are discarded. Declining here took the whole draw or dispatch with it -
|
||||
// the same shape as the unbound storage block fixed alongside this. A placeholder view
|
||||
// in the shader's own declared format lets the work proceed with the stores landing
|
||||
// nowhere anyone can observe, which is what GL asks for.
|
||||
const VkBufferView placeholder =
|
||||
AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding],
|
||||
programObj.samplerNumericDomainByBinding[binding], true);
|
||||
if (placeholder == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u, and the "
|
||||
"placeholder descriptor could not be created", imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveStorageTexelBufferDescriptor: image unit %d (binding %u) is unbound; using the "
|
||||
"placeholder descriptor", imageUnit, binding);
|
||||
outBufferView = placeholder;
|
||||
return true;
|
||||
}
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||
@@ -805,9 +1046,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||
imageUnit);
|
||||
return false;
|
||||
// Incomplete buffer texture, same as the sampled path: legal state, undefined data,
|
||||
// and no reason to drop the work.
|
||||
const VkBufferView placeholder =
|
||||
AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding],
|
||||
programObj.samplerNumericDomainByBinding[binding], true);
|
||||
if (placeholder == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer "
|
||||
"bound, and the placeholder descriptor could not be created", imageUnit);
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no attached GL buffer; "
|
||||
"using the placeholder descriptor", imageUnit);
|
||||
outBufferView = placeholder;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
|
||||
@@ -833,8 +1085,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
|
||||
// r32ui whatever the texture's own attachment format says. Falling back, in order:
|
||||
// reflected format, then the bind format, then the texture's attached format.
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
||||
const auto internalFormat = textureBuffer->GetFormat();
|
||||
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
@@ -905,23 +1155,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
||||
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
||||
binding);
|
||||
// An atomic counter is not an SSBO the application ever declared: glslang lowers every
|
||||
// atomic_uint onto a synthesized gl_AtomicCounterBlock_<N> storage block, where N is the
|
||||
// GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary
|
||||
// storage-block slot, so resolving it the SSBO way looked up GL_SHADER_STORAGE_BUFFER
|
||||
// point N' - which is never where glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, N, ...) put
|
||||
// the buffer. The counter therefore never reached the shader (KHR-GL43
|
||||
// shader_atomic_counters.advanced-usage-*), and when the application also bound an SSBO at
|
||||
// the colliding slot the descriptor silently aliased it, so the dispatch wrote over the
|
||||
// application's own buffer. DirectGLES has always taken this branch explicitly
|
||||
// (SyncAtomicCounterBuffers); this is the same rule in Magma's descriptor resolution.
|
||||
//
|
||||
// Only the SOURCE of the handle differs. The per-counter layout(offset=) is already folded
|
||||
// into the block's SPIR-V member offsets on this path (FlattenAtomicCounterBlockPass is
|
||||
// DirectGLES-only), so everything below - residency, the glBindBufferRange window, the
|
||||
// descriptor fill - is target-agnostic and stays exactly as it was.
|
||||
const String& blockName = programObj.storageBlockNameByBinding[binding];
|
||||
const Int atomicCounterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(blockName);
|
||||
const Bool isAtomicCounterBlock = atomicCounterBinding >= 0;
|
||||
const BufferTarget bufferTarget =
|
||||
isAtomicCounterBlock ? BufferTarget::AtomicCounter : BufferTarget::ShaderStorage;
|
||||
// A block instance array declares one block whose elements take consecutive GL binding
|
||||
// points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole
|
||||
// array to that one block - so the element index IS the offset from its binding.
|
||||
// array to that one block - so the element index IS the offset from its binding. glslang
|
||||
// synthesizes one counter block per GL binding, so a counter block is never an instance
|
||||
// array and `element` is always 0 there; the +element rule stays with the SSBO case.
|
||||
const GLuint frontendBinding =
|
||||
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||
isAtomicCounterBlock
|
||||
? static_cast<GLuint>(atomicCounterBinding)
|
||||
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||
const Uint32 bindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
|
||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||
"ResolveStorageBufferDescriptor: frontend SSBO binding %u out of range for block '%s'",
|
||||
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
||||
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
|
||||
frontendBinding, blockName.c_str());
|
||||
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
|
||||
// program declare a shader storage block the application never binds a buffer to:
|
||||
// the block simply has no store, so a read is undefined and a write goes nowhere.
|
||||
// Refusing here used to take the whole draw or dispatch with it (SetupDraw and
|
||||
// DispatchCompute both skip on a false return), which is how AcceleratedRendering's
|
||||
// GUI batch lost its backgrounds: its vertex-transform compute shader declares a
|
||||
// `Meshes` block it only reads when a vertex comes from a cached server mesh, and a
|
||||
// batch of plain GUI blits has no meshes and so binds nothing there. The dispatch
|
||||
// never ran, the transformed vertex buffer stayed as it was, and every hotbar and
|
||||
// container-screen background quad came out degenerate. A shared zero-filled
|
||||
// placeholder puts something legal in the descriptor and lets the draw proceed.
|
||||
const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor();
|
||||
if (!placeholder.IsValid()) {
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block "
|
||||
"'%s', and the placeholder descriptor could not be created",
|
||||
frontendBinding, blockName.c_str());
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveStorageBufferDescriptor: frontend binding %u ('%s') is unbound; using the placeholder "
|
||||
"descriptor",
|
||||
frontendBinding, blockName.c_str());
|
||||
outBufferInfo.buffer = placeholder.buffer;
|
||||
outBufferInfo.offset = placeholder.offset;
|
||||
outBufferInfo.range = placeholder.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The shader may write this buffer, and those writes land in GPU memory behind the
|
||||
@@ -997,8 +1293,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
return false;
|
||||
// Legal GL: an image unit with no texture bound makes loads return zero and discards
|
||||
// stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning
|
||||
// false here did - both SetupDraw and DispatchCompute skip everything on it. The
|
||||
// placeholder is a 1x1 image of the target and format the shader's declaration asks
|
||||
// for, so the descriptor is valid and the stores land where nobody can see them.
|
||||
TextureTarget placeholderTarget = TextureTarget::Unknown;
|
||||
VkFormat placeholderFormat = VK_FORMAT_UNDEFINED;
|
||||
SharedPtr<MG_State::GLState::ITextureObject> placeholder;
|
||||
if (ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget, placeholderFormat)) {
|
||||
placeholder = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat);
|
||||
}
|
||||
VkImageView placeholderView = VK_NULL_HANDLE;
|
||||
if (placeholder != nullptr &&
|
||||
m_textureManager->TransitionTextureForStorageImage(commandBuffer, *placeholder)) {
|
||||
// layered=true, layer=0: the placeholder's own view type IS the one the shader's
|
||||
// image declaration demands, and that is exactly what the layered form asks for
|
||||
// (see GetOrCreateStorageImageView, which only narrows the view type when a
|
||||
// non-layered binding names a single layer).
|
||||
placeholderView =
|
||||
m_textureManager->GetOrCreateStorageImageView(*placeholder, 0, placeholderFormat, true, 0);
|
||||
}
|
||||
if (placeholderView == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u, and no "
|
||||
"placeholder descriptor could be built (target=%d format=%d)",
|
||||
imageUnit, binding, static_cast<Int>(placeholderTarget),
|
||||
static_cast<Int>(placeholderFormat));
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ResolveStorageImageDescriptor: image unit %d (binding %u) is unbound; using the placeholder "
|
||||
"descriptor", imageUnit, binding);
|
||||
outImageInfo.sampler = VK_NULL_HANDLE;
|
||||
outImageInfo.imageView = placeholderView;
|
||||
outImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
return true;
|
||||
}
|
||||
|
||||
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
||||
@@ -1020,8 +1348,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
binding);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
|
||||
// The storage's own VkFormat is the wrong reference for a GL texture view: the view
|
||||
// reinterprets it (GL 4.6 core table 8.21), and it is the VIEW's format the shader's
|
||||
// image declaration was written against. Same correction the sampled path makes above.
|
||||
const VkFormat storageImageSourceFormat =
|
||||
imageBinding.Texture->IsTextureView()
|
||||
? m_textureManager->ResolveTextureViewWindow(*imageBinding.Texture, *resource).format
|
||||
: resource->format;
|
||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||
reflectedFormat, imageBinding.Format, storageImageSourceFormat, useBindingFormat);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||
@@ -1029,8 +1364,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
useBindingFormat ? "true" : "false");
|
||||
return false;
|
||||
}
|
||||
// glBindImageTexture named a level and a layer of the bound texture; on a GL texture
|
||||
// view both are relative to the view, and the storage image is what the descriptor
|
||||
// actually points at (see ToStorageMipLevel).
|
||||
const Int32 storageImageLayer =
|
||||
imageBinding.Layered != GL_FALSE
|
||||
? imageBinding.Layer
|
||||
: static_cast<Int32>(ToStorageArrayLayer(imageBinding.Texture.get(), imageBinding.Layer));
|
||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||
*imageBinding.Texture, ToStorageMipLevel(imageBinding.Texture.get(), static_cast<Int>(mipLevel)),
|
||||
viewFormat, imageBinding.Layered != GL_FALSE, storageImageLayer);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||
@@ -1046,18 +1389,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return outImageInfo.imageView != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
||||
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
|
||||
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
|
||||
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
|
||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||
// process over an unbound sampler is never the right answer.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// A multisample sampler cannot be served by the single-sampled 2D image below - its
|
||||
// descriptor demands a multisample view - so it gets its own placeholder rather than no
|
||||
// placeholder at all. Without one, ResolveSamplerDescriptor declined and
|
||||
// BindProgramUniformBuffers dropped the WHOLE draw, which is how every
|
||||
// sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
|
||||
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
|
||||
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
|
||||
// incomplete texture is undefined, not fatal, so the draw has to happen.
|
||||
if (target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray) {
|
||||
return GetFallbackMultisampleTexture(target, numericDomain);
|
||||
}
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
|
||||
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
|
||||
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
|
||||
// image can never carry that bit, which is why the arm above needs one object per domain.
|
||||
if (m_fallbackTexture2D == nullptr) {
|
||||
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
||||
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
@@ -1075,6 +1430,215 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return m_fallbackTexture2D;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
|
||||
//
|
||||
// A descriptor whose image format is in a different numeric class than the sampler that
|
||||
// reads it needs a format-reinterpreting view, and building one needs
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
|
||||
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
|
||||
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
|
||||
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
|
||||
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
|
||||
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
|
||||
// image's own format to the sampler's class instead means no reinterpreting view is
|
||||
// needed at all.
|
||||
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
|
||||
Uint32 domainSlot = 0;
|
||||
switch (numericDomain) {
|
||||
case SamplerNumericDomain::SignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8I;
|
||||
domainSlot = 1;
|
||||
break;
|
||||
case SamplerNumericDomain::UnsignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8UI;
|
||||
domainSlot = 2;
|
||||
break;
|
||||
case SamplerNumericDomain::Float:
|
||||
case SamplerNumericDomain::Unknown:
|
||||
default:
|
||||
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
|
||||
// a shader whose sampler class could not be reflected is far likelier to be a plain
|
||||
// sampler2DMS than an integer one, and a float view is the only one buildable without
|
||||
// the mutable bit anyway.
|
||||
break;
|
||||
}
|
||||
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
|
||||
auto cached = m_fallbackMultisampleTextures.find(key);
|
||||
if (cached != m_fallbackMultisampleTextures.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
|
||||
: TextureUploadTarget::Texture2DMultisample;
|
||||
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
|
||||
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
|
||||
if (arrayed) {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
|
||||
} else {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
|
||||
}
|
||||
texture->SetInternalFormat(internalFormat);
|
||||
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
|
||||
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
|
||||
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
|
||||
// that re-created it would be worse than none.
|
||||
texture->SetSamples(2);
|
||||
texture->SetFixedSampleLocations(true);
|
||||
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
|
||||
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
|
||||
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
|
||||
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
|
||||
// texture that is not complete. The point of the placeholder is that the DRAW happens.
|
||||
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
texture->MarkStorageDirty(uploadTarget, 0, false);
|
||||
// Worth knowing if it ever fires: an integer multisample format can legitimately support
|
||||
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
|
||||
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
|
||||
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
|
||||
// above exists to avoid. It already warns from there; nothing better is available - a
|
||||
// one-sample integer image is still a draw, and declining is the outcome this whole
|
||||
// placeholder replaced.
|
||||
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
|
||||
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
|
||||
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
|
||||
}
|
||||
|
||||
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
|
||||
SamplerNumericDomain numericDomain, Bool storage) {
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null");
|
||||
const VkFormatFeatureFlags requiredFeature = storage ? VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT
|
||||
: VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT;
|
||||
const VkFormat fallbackFormat = PlaceholderFormatForNumericDomain(numericDomain);
|
||||
|
||||
VkFormat format = declaredFormat;
|
||||
if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, requiredFeature)) {
|
||||
// The declared format is what a shader that WRITES through this descriptor is
|
||||
// validated against, so it is tried first and kept whenever the device can use it.
|
||||
// Falling back is for the two cases where it cannot be: a sampled texel buffer, which
|
||||
// declares no format at all, and a device that does not list the declared one as a
|
||||
// texel buffer. The fallback stays inside the shader's numeric class, which is the
|
||||
// part the descriptor is checked on for a formatless declaration - and the R32
|
||||
// members of the three classes are mandatory-support formats, so this cannot fail for
|
||||
// want of device features.
|
||||
format = fallbackFormat;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED || !BufferFormatSupportsFeature(m_physicalDevice, format, requiredFeature)) {
|
||||
MGLOG_E_ONCE("AcquireUnboundTexelBufferView: no usable placeholder format (declared=%d fallback=%d "
|
||||
"storage=%s)",
|
||||
static_cast<Int>(declaredFormat), static_cast<Int>(fallbackFormat),
|
||||
storage ? "true" : "false");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const Uint64 key = (static_cast<Uint64>(format) << 1) | (storage ? 1ull : 0ull);
|
||||
const auto cached = m_unboundTexelBufferViews.find(key);
|
||||
if (cached != m_unboundTexelBufferViews.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const BufferSlice placeholder = m_bufferManager->AcquireUnboundTexelBufferDescriptor();
|
||||
if (!placeholder.IsValid()) {
|
||||
MGLOG_E_ONCE("AcquireUnboundTexelBufferView: placeholder buffer unavailable");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A buffer view's range must be a whole number of texels of its own format, and the
|
||||
// placeholder is sized for the largest of them - so floor rather than assume.
|
||||
const VkDeviceSize texelSize = std::max<VkDeviceSize>(1, vkuFormatTexelBlockSize(format));
|
||||
const VkDeviceSize range = (placeholder.size / texelSize) * texelSize;
|
||||
if (range == 0) {
|
||||
MGLOG_E_ONCE("AcquireUnboundTexelBufferView: placeholder holds no whole texel of format=%d",
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
VkBufferViewCreateInfo viewInfo{};
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
||||
viewInfo.buffer = placeholder.buffer;
|
||||
viewInfo.format = format;
|
||||
viewInfo.offset = placeholder.offset;
|
||||
viewInfo.range = range;
|
||||
|
||||
VkBufferView view = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &view);
|
||||
if (result != VK_SUCCESS || view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("AcquireUnboundTexelBufferView: vkCreateBufferView failed result=%d format=%d", result,
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_unboundTexelBufferViews.emplace(key, view);
|
||||
MGLOG_D("AcquireUnboundTexelBufferView: created placeholder view format=%d storage=%s",
|
||||
static_cast<Int>(format), storage ? "true" : "false");
|
||||
return view;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, TextureTarget& outTarget,
|
||||
VkFormat& outFormat) const {
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
"ResolveUnboundStorageImagePlaceholder: binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveUnboundStorageImagePlaceholder: format binding %u out of range", binding);
|
||||
outTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
// The shader's own format qualifier, exactly as the bound path prefers it over the one
|
||||
// glBindImageTexture named - there is no binding here to name one. A `writeonly` image
|
||||
// may carry no qualifier at all; its numeric class is then the only constraint, and the
|
||||
// R32 member of that class is what carries it (see AcquireUnboundTexelBufferView).
|
||||
outFormat = programObj.storageImageFormatByBinding[binding];
|
||||
if (outFormat == VK_FORMAT_UNDEFINED) {
|
||||
outFormat = PlaceholderFormatForNumericDomain(programObj.samplerNumericDomainByBinding[binding]);
|
||||
}
|
||||
return outFormat != VK_FORMAT_UNDEFINED && PlaceholderShapeForTarget(outTarget).valid;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetUnboundStorageImageTexture(
|
||||
TextureTarget target, VkFormat format) const {
|
||||
const Uint64 key = (static_cast<Uint64>(target) << 32) | static_cast<Uint32>(format);
|
||||
const auto cached = m_unboundStorageImageTextures.find(key);
|
||||
if (cached != m_unboundStorageImageTextures.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const PlaceholderShape shape = PlaceholderShapeForTarget(target);
|
||||
if (!shape.valid) {
|
||||
// A multisample image uniform is the case with no answer here: its descriptor demands
|
||||
// a multisample view, and a single-sampled 1x1 image is invalid Vulkan in that slot,
|
||||
// not a degraded picture. The caller declines the binding exactly as it did before.
|
||||
MGLOG_D("GetUnboundStorageImageTexture: no placeholder shape for target=%d", static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
const TextureInternalFormat internalFormat = InternalFormatForVkFormat(format);
|
||||
if (internalFormat == TextureInternalFormat::Unknown) {
|
||||
MGLOG_E_ONCE("GetUnboundStorageImageTexture: no GL internal format matches VkFormat=%d",
|
||||
static_cast<Int>(format));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = MakePlaceholderTextureObject(target, kUnboundStorageImageExternalIndex);
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
texture->SetInternalFormat(internalFormat);
|
||||
const SizeT texelBytes = MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat);
|
||||
for (Uint32 index = 0; index < shape.uploadTargetCount; ++index) {
|
||||
texture->AllocateStorage(shape.uploadTargets[index], 0,
|
||||
{.texelSize = {1, 1, shape.depth},
|
||||
.byteSize = texelBytes * static_cast<SizeT>(shape.depth)});
|
||||
// Not dirty: there is deliberately nothing to upload. The image is created and
|
||||
// transitioned to GENERAL by the storage-image preparation pass like any other, and
|
||||
// its contents are exactly as undefined as GL says a fetch through an unbound image
|
||||
// unit is.
|
||||
texture->MarkStorageDirty(shape.uploadTargets[index], 0, false);
|
||||
}
|
||||
m_unboundStorageImageTextures.emplace(key, texture);
|
||||
MGLOG_D("GetUnboundStorageImageTexture: created placeholder target=%d format=%d", static_cast<Int>(target),
|
||||
static_cast<Int>(format));
|
||||
return texture;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, Uint32 element,
|
||||
@@ -1098,25 +1662,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
// The sampler in effect, resolved BEFORE the completeness test below rather than after:
|
||||
// GL's completeness rules are a property of (texture, sampler in effect), so the test
|
||||
// cannot be asked without it.
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
const MG_State::GLState::SamplerObject* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get()
|
||||
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
|
||||
// like ResolveSamplerTextureRaw reports it.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||
texture = nullptr;
|
||||
}
|
||||
// ...and so does a texture that fails the completeness rules for the filter in effect,
|
||||
// because that is precisely what ResolveSamplerDescriptor does with it. The two used to
|
||||
// disagree: this one asked only whether the default texture was UNDEFINED, so a default
|
||||
// texture that had been given a base level but no mip chain - which is what the GL-CTS
|
||||
// state reset between test cases leaves behind, and what any application that uploads to
|
||||
// texture 0 has - stayed in the sampled set while the descriptor path swapped it for the
|
||||
// fallback. SetupDraw then synced a texture no descriptor would use, the sync declined
|
||||
// (GL calls it incomplete), and the null it returned was dereferenced one line later.
|
||||
// Keeping the two predicates identical is the invariant; CollectSampledTextures exists to
|
||||
// pre-sync exactly the textures the descriptors will hold.
|
||||
if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
// Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
|
||||
// a multisample arm and the two must not drift apart.
|
||||
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
// The substitution changed the texture, so the "no override" arm of the effective
|
||||
// sampler has to follow it to the fallback's own.
|
||||
if (!samplerOverride) {
|
||||
effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr;
|
||||
}
|
||||
}
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
outTexture = texture;
|
||||
outSampler = samplerOverride ? samplerOverride.get()
|
||||
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||
outSampler = effectiveSampler;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1260,9 +1848,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
// ResolveStorageImageDescriptor will substitute the placeholder image for this
|
||||
// binding; include it here for the same reason the sampled walk includes the
|
||||
// fallback texture - this walk is what gets a storage image created,
|
||||
// STORAGE-usage-marked and transitioned to GENERAL BEFORE the render pass
|
||||
// opens, and all three of those are illegal once it has. A target with no
|
||||
// placeholder shape (multisample) contributes nothing and is declined at
|
||||
// resolve time exactly as it was.
|
||||
TextureTarget placeholderTarget = TextureTarget::Unknown;
|
||||
VkFormat placeholderFormat = VK_FORMAT_UNDEFINED;
|
||||
if (!ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget,
|
||||
placeholderFormat)) {
|
||||
continue;
|
||||
}
|
||||
texture = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat).get();
|
||||
if (texture == nullptr) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
|
||||
outTextures.push_back(texture);
|
||||
@@ -1298,9 +1900,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
||||
sampledTexture, sampledSampler) ||
|
||||
sampledTexture == nullptr || sampledSampler == nullptr ||
|
||||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
|
||||
IsPlaceholderTexture(sampledTexture)) {
|
||||
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
||||
// alias the image-unit binding of the original texture.
|
||||
// alias the image-unit binding of the original texture. The unbound and
|
||||
// incomplete cases both arrive here AS that fallback now that
|
||||
// ResolveSampledBinding applies the completeness rule itself, so the test is
|
||||
// "is this one of ours" rather than a second completeness check.
|
||||
continue;
|
||||
}
|
||||
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
||||
|
||||
@@ -42,7 +42,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
|
||||
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
|
||||
// texel buffer, and there is no other route to that answer from here.
|
||||
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||
@@ -176,7 +179,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
|
||||
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
|
||||
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
|
||||
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
|
||||
// one shared object, because a descriptor whose format disagrees with the shader's
|
||||
// declaration is invalid Vulkan even when nothing ever reads it.
|
||||
//
|
||||
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
|
||||
// texel buffer, which never carries one, or for a formatless `writeonly` image);
|
||||
// `numericDomain` decides the format when there is no declaration and is the fallback
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
@@ -251,6 +290,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet);
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VkBufferManager* m_bufferManager = nullptr;
|
||||
ProgramFactory* m_programFactory = nullptr;
|
||||
Vector<FrameResources> m_frames;
|
||||
@@ -263,6 +303,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
|
||||
@@ -16,6 +16,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
constexpr SizeT kLiveResourcePruneThreshold = 256;
|
||||
|
||||
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
|
||||
// every minStorageBufferOffsetAlignment in the wild, and free.
|
||||
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
|
||||
// See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
|
||||
// same reason plus one: a texel buffer view's range must be a whole number of texels of
|
||||
// whatever format the placeholder is asked for, and 256 divides by every texel size in
|
||||
// the GL image-format table (1, 2, 4, 8 and 16 bytes).
|
||||
constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;
|
||||
|
||||
// A zero-copy persistent buffer is created once and never recreated (the app holds
|
||||
// its mapped pointer), and may be bound to any role, so it carries every usage.
|
||||
// TRANSFER_DST is added by CreateResidentStorage.
|
||||
@@ -130,6 +139,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
m_transientUploadArena.Shutdown();
|
||||
m_unboundStorageBuffer.Destroy();
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
DestroyAllDeferredReleases();
|
||||
ReleaseAllLiveResources();
|
||||
m_copyProvider = nullptr;
|
||||
@@ -706,6 +717,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredResourceReleases[frameIndex].clear();
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
|
||||
if (!m_unboundStorageBuffer.IsValid()) {
|
||||
if (m_initInfo.allocator == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// Host-visible so the zero fill needs no command buffer: this can be reached from
|
||||
// descriptor resolution, which runs inside an already-open recording and must not
|
||||
// start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
|
||||
// block rather than 4 bytes so that a shader which does read the block gets a
|
||||
// plausible unsized-array length instead of one that rounds to zero.
|
||||
const Bool created = m_unboundStorageBuffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = kUnboundStorageDescriptorBytes,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
});
|
||||
if (!created) {
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
|
||||
m_unboundStorageBuffer.Destroy();
|
||||
return {};
|
||||
}
|
||||
if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
|
||||
Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
|
||||
}
|
||||
}
|
||||
return m_unboundStorageBuffer.GetSlice();
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
|
||||
if (!m_unboundTexelBuffer.IsValid()) {
|
||||
if (m_initInfo.allocator == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// A SECOND placeholder rather than more usage bits on the storage-block one. The two
|
||||
// are independent failure domains: a device that refuses this allocation must not
|
||||
// take the storage-block placeholder - and with it the fix this one is a sibling of -
|
||||
// down with it. Host-visible and zero-filled for the same reason as that one: this is
|
||||
// reached from descriptor resolution, inside an already-open recording, which must
|
||||
// not start a copy of its own.
|
||||
const Bool created = m_unboundTexelBuffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = kUnboundTexelBufferDescriptorBytes,
|
||||
.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
});
|
||||
if (!created) {
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
return {};
|
||||
}
|
||||
if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
|
||||
Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
|
||||
}
|
||||
}
|
||||
return m_unboundTexelBuffer.GetSlice();
|
||||
}
|
||||
|
||||
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
|
||||
@@ -120,6 +120,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
|
||||
VkDeviceSize alignment, BufferSlice& outSlice);
|
||||
|
||||
// The descriptor a shader storage block gets when the program declares it and the
|
||||
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
|
||||
// legal state - the block simply has no store, so reads are undefined and writes go
|
||||
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
|
||||
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
|
||||
// buffer, created once and shared by every unbound binding: bindings that are only
|
||||
// declared (the case this exists for) never touch it, and one that is actually read
|
||||
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
|
||||
// that indexes past it.
|
||||
BufferSlice AcquireUnboundStorageDescriptor();
|
||||
|
||||
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
|
||||
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
|
||||
// it has no GL buffer attached. Both are legal GL states that make a fetch return
|
||||
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
|
||||
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
|
||||
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
|
||||
// and the descriptor is a VkBufferView, not a buffer - is built by
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
@@ -180,6 +201,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkBufferManagerInitInfo m_initInfo{};
|
||||
BufferArena m_transientUploadArena;
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
// with the manager.
|
||||
VkBufferObject m_unboundStorageBuffer;
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#include "VkClearManager.h"
|
||||
|
||||
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
|
||||
// clear key's layer span has to be the same one the render pass builds its attachment view from.
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentLayerCount(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
|
||||
// the entry - so a layered cube map's glClear reached one face and the other five were lost
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
@@ -122,8 +126,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -157,6 +183,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -286,9 +321,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(texture.get());
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -305,8 +342,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -321,6 +364,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -411,6 +455,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
|
||||
@@ -67,28 +67,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
|
||||
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
|
||||
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
|
||||
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
|
||||
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
|
||||
// they resolve the layer through their own copies of this helper, which do shift.
|
||||
const auto* texture = attachment.GetTexture().get();
|
||||
if (attachment.IsLayered()) {
|
||||
return 0;
|
||||
return ToStorageArrayLayer(texture, 0);
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
|
||||
}
|
||||
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
const Int face =
|
||||
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
|
||||
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
|
||||
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
|
||||
|
||||
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
|
||||
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
|
||||
// type is not a legal answer for several of the targets GL can attach, and returning it
|
||||
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
|
||||
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
|
||||
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
|
||||
// or dereferenced - by the driver inside vkCreateFramebuffer.
|
||||
//
|
||||
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
|
||||
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
|
||||
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
|
||||
//
|
||||
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
|
||||
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
|
||||
// the only honest answer would be to decline the attachment - and every driver this has run on,
|
||||
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
|
||||
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
|
||||
static VkImageViewType ResolveAttachmentViewType(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const VkTextureManager::TextureResource& resource) {
|
||||
if (attachment.IsLayered()) {
|
||||
return resource.viewType;
|
||||
switch (resource.viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
default:
|
||||
return resource.viewType;
|
||||
}
|
||||
}
|
||||
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||
@@ -96,8 +126,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||
// symmetry with CUBE_ARRAY.
|
||||
//
|
||||
// 3D belongs in the same list and was missing from it, which is why the "per-slice
|
||||
// attachment view is a 2D view whose array layer is the slice" branch in
|
||||
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
|
||||
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
|
||||
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
|
||||
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return resource.viewType;
|
||||
@@ -318,47 +355,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
|
||||
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
|
||||
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
|
||||
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
|
||||
//
|
||||
// This used to be a hand-maintained second copy of that table, and it was missing exactly
|
||||
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
|
||||
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
|
||||
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
|
||||
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
|
||||
// a size-incompatible vkCmdCopyImage.
|
||||
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -633,11 +641,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
|
||||
att.GetTextureLevel()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
|
||||
att.GetTextureLayer()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
@@ -754,7 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
@@ -840,7 +850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,13 +874,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
@@ -993,8 +1003,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
|
||||
"%u on FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||
continue;
|
||||
@@ -1006,7 +1020,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
|
||||
auto& att = fbo.GetAttachment(drawbuf);
|
||||
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
|
||||
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
|
||||
const auto textureTarget = texture->GetTarget();
|
||||
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||
attachmentDescriptions.emplace_back();
|
||||
@@ -1049,8 +1063,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
const IntVec2 attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent);
|
||||
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
|
||||
// 1D-array attachment's GL height is its layer count, and using it as the
|
||||
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
|
||||
// image it is built over.
|
||||
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
|
||||
if (width == 0)
|
||||
width = attachmentExtent.x();
|
||||
if (height == 0)
|
||||
@@ -1078,8 +1096,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
if (textureResource == nullptr) {
|
||||
// SyncTextureResource legitimately declines - an unsupported format,
|
||||
// sample count or image-flag combination, or a vkCreateImage the driver
|
||||
// refused. There is no image to attach, so there is no render pass.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
|
||||
"attachment %u on FBO %u; declining the render pass",
|
||||
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
@@ -1100,8 +1125,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
|
||||
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
|
||||
"render pass",
|
||||
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
desc.samples = attachmentSampleCount;
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
@@ -1144,7 +1176,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (a.IsTexture() && b.IsTexture()) {
|
||||
return a.GetTexture().get() == b.GetTexture().get() &&
|
||||
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
|
||||
a.GetTextureLevel() == b.GetTextureLevel();
|
||||
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
|
||||
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
|
||||
}
|
||||
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
|
||||
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
|
||||
@@ -1193,20 +1226,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
|
||||
MOBILEGL_ASSERT(depthTextureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
|
||||
if (depthTextureResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthTextureResource->layout;
|
||||
depthAttachmentDescription.format = depthTextureResource->format;
|
||||
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
||||
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
|
||||
attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
|
||||
swapchainExtent);
|
||||
attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo,
|
||||
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
|
||||
swapchainExtent);
|
||||
} else {
|
||||
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
||||
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
MOBILEGL_ASSERT(depthRenderbufferResource,
|
||||
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
|
||||
if (depthRenderbufferResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthRenderbufferResource->layout;
|
||||
depthAttachmentDescription.format = depthRenderbufferResource->format;
|
||||
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
|
||||
@@ -1254,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
const Uint32 attachmentMipLevel =
|
||||
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
|
||||
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
|
||||
selectedDepthStencilAttachment->GetTextureLevel());
|
||||
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
||||
@@ -1279,8 +1322,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
|
||||
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1302,6 +1351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(depthRenderbufferResource->view);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1399,8 +1454,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
|
||||
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
|
||||
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
|
||||
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
|
||||
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
|
||||
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
|
||||
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
|
||||
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
|
||||
// line on every draw for the life of the process - a decline caches nothing, so every
|
||||
// later draw to the same framebuffer re-enters this path and fails again.
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
const VkResult renderPassResult =
|
||||
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
|
||||
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
|
||||
"render pass",
|
||||
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
VkFramebufferCreateInfo framebufferCreateInfo;
|
||||
@@ -1413,8 +1485,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
framebufferCreateInfo.width = width;
|
||||
framebufferCreateInfo.height = height;
|
||||
framebufferCreateInfo.layers = framebufferLayers;
|
||||
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer));
|
||||
const VkResult framebufferResult =
|
||||
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
|
||||
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
|
||||
// The render pass has no entry to own it yet, so it is destroyed here rather than
|
||||
// leaked - RenderPassEntry's destructor is the only other thing that would.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
|
||||
"%u attachments, %u layers); declining the render pass",
|
||||
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
|
||||
fbo.GetExternalIndex(), width, height,
|
||||
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
|
||||
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
IntVec2 extent = {width, height};
|
||||
RenderPassEntry renderPassEntry {
|
||||
hash,
|
||||
@@ -1439,7 +1524,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
return insertedIt->second;
|
||||
return &insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
|
||||
@@ -243,9 +243,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
|
||||
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
}
|
||||
|
||||
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
|
||||
// family and an integer family and requires the sampler's choice to match the image view's
|
||||
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
|
||||
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
|
||||
enum class BorderColorDomain {
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger
|
||||
};
|
||||
|
||||
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return BorderColorDomain::SignedInteger;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return BorderColorDomain::UnsignedInteger;
|
||||
default:
|
||||
return BorderColorDomain::Float;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
|
||||
// format in which texture components are stored. For signed and unsigned normalized
|
||||
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
|
||||
// floating-point and integer formats, border values are clamped to the representable range of
|
||||
// the format." Every clause of that sentence is a real case here - the clamp is not just the
|
||||
// normalized one.
|
||||
//
|
||||
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
|
||||
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
|
||||
// so a negative border on them must come back as 0 rather than as a negative number the
|
||||
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
|
||||
struct FloatBorderRange {
|
||||
Bool clamped = true;
|
||||
Float minValue = 0.0f;
|
||||
Float maxValue = 1.0f;
|
||||
};
|
||||
|
||||
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
return {false, 0.0f, 0.0f};
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
return {true, -65504.0f, 65504.0f};
|
||||
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
|
||||
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
|
||||
// that matters for correctness is the lower one, and a single conservative upper bound
|
||||
// costs nothing a real border colour will ever notice.
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
return {true, 0.0f, 64512.0f};
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
return {true, 0.0f, 65408.0f};
|
||||
default:
|
||||
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Per-component representable range of an integer texture format, as Int64 so that the whole
|
||||
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
|
||||
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
|
||||
// whose alpha is narrower than its colour channels.
|
||||
struct IntegerBorderRange {
|
||||
Int64 rgbMin = 0;
|
||||
Int64 rgbMax = 0;
|
||||
Int64 alphaMin = 0;
|
||||
Int64 alphaMax = 0;
|
||||
};
|
||||
|
||||
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
|
||||
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
return uniform(-128, 127);
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
return uniform(-32768, 32767);
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
return uniform(0, 255);
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
return uniform(0, 65535);
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
return uniform(0, 4294967295LL);
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return {0, 1023, 0, 3};
|
||||
default:
|
||||
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
|
||||
// clamp that cannot alter a value the GL entry points could have carried.
|
||||
return uniform(-2147483648LL, 2147483647LL);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsDepthTextureFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
m_customBorderColorSupported = initInfo.customBorderColorSupported;
|
||||
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
m_customBorderColorSupported = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.borderColor = borderColor.color;
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
|
||||
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
|
||||
// customBorderColorWithoutFormat was required alongside customBorderColors at device
|
||||
// creation - a GL sampler object has no idea which texture it will be paired with.
|
||||
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
|
||||
if (borderColor.isCustom) {
|
||||
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
|
||||
customBorderColorInfo.customBorderColor = borderColor.customValue;
|
||||
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
|
||||
customBorderColorInfo.pNext = samplerInfo.pNext;
|
||||
samplerInfo.pNext = &customBorderColorInfo;
|
||||
}
|
||||
|
||||
VkSampler vkSampler = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
|
||||
|
||||
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.usesCustomBorderColor = borderColor.isCustom;
|
||||
if (entry.usesCustomBorderColor) {
|
||||
++m_customBorderColorSamplerCount;
|
||||
}
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
ResolvedBorderColor resolved{};
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
}
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
const auto& borderColor = sampler.GetBorderColor();
|
||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||
const auto format = texture.GetFormat();
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
|
||||
if (isDepthTexture) {
|
||||
if (domain != BorderColorDomain::Float) {
|
||||
// An integer image view REQUIRES an integer border colour, whatever the value is - even
|
||||
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
|
||||
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
|
||||
// representation (a plain cast) is as good an answer as any.
|
||||
//
|
||||
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
|
||||
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
|
||||
//
|
||||
// Which representation to start from is the TEXTURE's domain, not the entry-point form
|
||||
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
|
||||
// an integer internal data type and does not define a sign conversion between the two
|
||||
// integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
|
||||
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
|
||||
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
|
||||
// the representation by the FORM instead would answer 0 here, which is a defensible
|
||||
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
|
||||
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
|
||||
// agreement decides it.
|
||||
const auto range = ResolveIntegerBorderRange(format);
|
||||
const auto& borderColorI = sampler.GetBorderColorI();
|
||||
const auto& borderColorUI = sampler.GetBorderColorUI();
|
||||
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
|
||||
Int64 clamped[4];
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
|
||||
: static_cast<Int64>(borderColorI[channel]);
|
||||
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
|
||||
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
|
||||
clamped[channel] = std::clamp(raw, low, high);
|
||||
}
|
||||
|
||||
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
|
||||
// the palette entry it clamps to rather than missing every one of them.
|
||||
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
|
||||
if (allZeroRgb && clamped[3] == 0) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (allZeroRgb && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (domain == BorderColorDomain::UnsignedInteger) {
|
||||
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
|
||||
} else {
|
||||
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
// No custom colour available: pick the nearest of the three integer palette entries
|
||||
// rather than always answering transparent black, which is what turned an integer border
|
||||
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
|
||||
const Bool opaque = clamped[3] != 0;
|
||||
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
|
||||
// format, so it is clamped to that format's representable range first. Without the clamp the
|
||||
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
|
||||
// and fell through to transparent black - every border texel sampled 0 where the test wanted
|
||||
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
|
||||
// 32-bit float formats are unclamped.
|
||||
FloatVec4 borderColor = sampler.GetBorderColor();
|
||||
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
|
||||
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.y(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.z(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.w(), range.minValue, range.maxValue));
|
||||
}
|
||||
|
||||
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
|
||||
if (IsDepthTextureFormat(format)) {
|
||||
if (NearlyEqual(borderColor.x(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
|
||||
NearlyEqual(borderColor.z(), 0.0f);
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
|
||||
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
resolved.customValue.float32[0] = borderColor.x();
|
||||
resolved.customValue.float32[1] = borderColor.y();
|
||||
resolved.customValue.float32[2] = borderColor.z();
|
||||
resolved.customValue.float32[3] = borderColor.w();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Nearest of the three float palette entries. Transparent black stays the answer for a
|
||||
// transparent border, which is what the old unconditional fallback got right by accident.
|
||||
const Bool opaque = borderColor.w() >= 0.5f;
|
||||
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -28,6 +28,13 @@ public:
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
|
||||
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
|
||||
Bool customBorderColorSupported = false;
|
||||
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
|
||||
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
|
||||
// and falls back to the snapped predefined value once it is reached.
|
||||
Uint32 maxCustomBorderColorSamplers = 0;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
@@ -52,6 +59,21 @@ public:
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
|
||||
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
|
||||
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
|
||||
//
|
||||
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
|
||||
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
|
||||
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
|
||||
// custom colour was affordable is decided before the key is built, not twice with a budget
|
||||
// change in between.
|
||||
struct ResolvedBorderColor {
|
||||
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
VkClearColorValue customValue{};
|
||||
Bool isCustom = false;
|
||||
};
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
@@ -60,17 +82,18 @@ private:
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
|
||||
Bool usesCustomBorderColor = false;
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
|
||||
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
@@ -82,6 +105,12 @@ private:
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
Bool m_customBorderColorSupported = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
|
||||
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
|
||||
// and Shutdown.
|
||||
Uint32 m_customBorderColorSamplerCount = 0;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
|
||||
@@ -46,13 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
struct TextureShapeInfo {
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
@@ -220,6 +213,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
|
||||
MG_State::GLState::ITextureObject* texture) {
|
||||
// A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it
|
||||
// views, not by itself. Everything this identity keys - the TextureResource, the tracked
|
||||
// image layout, the alive-object weak reference, the storage-usage marks, the per-draw
|
||||
// sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing
|
||||
// the resolution here rather than at each call site is what makes it impossible to miss
|
||||
// one: a layout update posted against a view's own identity would have found no resource
|
||||
// at all, which is precisely how an attached view came back blank.
|
||||
//
|
||||
// One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the
|
||||
// root at creation, so a storage owner is never itself a view.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity{
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -364,7 +373,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
@@ -694,6 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||
m_viewRequestedImageFlags.erase(identity);
|
||||
m_viewRequestedFormats.erase(identity);
|
||||
auto resourceIt = m_textureResources.find(identity);
|
||||
if (resourceIt != m_textureResources.end()) {
|
||||
DeferResourceRelease(Move(resourceIt->second));
|
||||
@@ -737,9 +748,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_drawSyncedThisDraw.clear();
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
||||
|
||||
// A GL texture view has no image of its own; it resolves to - and shares - the resource
|
||||
// of the texture whose storage it views, so that there is exactly one VkImage, one
|
||||
// tracked layout and one upload path per storage. Everything that makes the view a
|
||||
// different texture (format, level/layer window, sampled aspect) is applied where the
|
||||
// VkImageViews are built, keyed in alternateSampledViews / attachmentViews.
|
||||
MG_State::GLState::ITextureObject& texture = StorageTextureOf(textureOrView);
|
||||
if (&texture != &textureOrView) {
|
||||
NoteTextureViewImageRequirements(textureOrView, texture);
|
||||
}
|
||||
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
|
||||
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
|
||||
@@ -838,7 +859,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A GL texture view shares this resource with the texture it views, so it must not touch
|
||||
// perMipViews: that vector is indexed by mip level alone and holds views built with the
|
||||
// STORAGE texture's format and full layer range. Route it through the keyed attachment
|
||||
// cache instead, where its own window is part of the key.
|
||||
if (texture.IsTextureView()) {
|
||||
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
|
||||
window.viewType);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -866,21 +899,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 layerCount,
|
||||
VkImageViewType viewType) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
|
||||
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
|
||||
// SyncTextureResource asks for and may have had refused per format.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
|
||||
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
|
||||
// view still contributes here is its own internal format, which may reinterpret the
|
||||
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
|
||||
// through.
|
||||
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
|
||||
if (texture.IsTextureView()) {
|
||||
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
|
||||
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
|
||||
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
|
||||
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
|
||||
// means). BOTH spellings are legal only on a 2D-array-compatible image
|
||||
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
|
||||
// may have had refused per format.
|
||||
//
|
||||
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
|
||||
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
|
||||
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
|
||||
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
|
||||
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
|
||||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
|
||||
"2D-array-compatible=%d)",
|
||||
// Not an error line: the render-pass builder turns the null view into one
|
||||
// MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
|
||||
"slices, 2D-array-compatible=%d)",
|
||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||
mipLevel, sliceCount,
|
||||
static_cast<Int>(viewType), mipLevel, sliceCount,
|
||||
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -894,10 +949,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
|
||||
const VkFormat baseAttachmentFormat =
|
||||
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
|
||||
const VkFormat attachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
|
||||
|
||||
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType) {
|
||||
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
|
||||
// for a view every field below is part of what distinguishes it from its parent.
|
||||
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
|
||||
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -932,7 +992,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
|
||||
// own format and aspect, so a GL view has to go to the keyed cache.
|
||||
if (texture.IsTextureView()) {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
|
||||
window.baseMipLevel = mipLevel;
|
||||
window.levelCount = 1;
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -960,14 +1035,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
|
||||
resource->sampledView == VK_NULL_HANDLE) {
|
||||
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
|
||||
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
|
||||
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
|
||||
// in the same cache while both name the same image, the same levels and the same layers.
|
||||
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource,
|
||||
const TextureViewWindow& window) {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = window.baseMipLevel,
|
||||
.levelCount = window.levelCount,
|
||||
.baseArrayLayer = window.baseArrayLayer,
|
||||
.layerCount = window.layerCount,
|
||||
.viewType = window.viewType,
|
||||
.format = window.format,
|
||||
.aspect = window.sampledAspect,
|
||||
.componentSwizzle = PackComponentSwizzle(window.components),
|
||||
};
|
||||
const auto existing = resource.alternateSampledViews.find(key);
|
||||
if (existing != resource.alternateSampledViews.end()) {
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
if (window.format != resource.format &&
|
||||
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
|
||||
"(image format=%d)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Int>(resource.format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const VkImageView view =
|
||||
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
|
||||
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
|
||||
&window.components);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
|
||||
"mips=[%u,%u) layers=[%u,%u)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
|
||||
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
|
||||
window.baseArrayLayer + window.layerCount);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
resource.alternateSampledViews.emplace(key, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// A GL texture view never has a sampledView of its own on this resource - that one
|
||||
// belongs to the storage texture, with the storage texture's format, level range and
|
||||
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
|
||||
// keyed cache, even when the requested format happens to match the image's.
|
||||
if (texture.IsTextureView()) {
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
if (format != VK_FORMAT_UNDEFINED) {
|
||||
window.format = format;
|
||||
}
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
|
||||
if (resource->sampledView == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
|
||||
return resource->sampledView;
|
||||
}
|
||||
@@ -987,8 +1124,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = resource->sampledBaseMipLevel,
|
||||
.levelCount = resource->sampledLevelCount,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = resource->arrayLayers,
|
||||
.viewType = resource->viewType,
|
||||
.format = format,
|
||||
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.componentSwizzle = PackComponentSwizzle(
|
||||
ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()))),
|
||||
};
|
||||
const auto existing = resource->alternateSampledViews.find(key);
|
||||
if (existing != resource->alternateSampledViews.end()) {
|
||||
@@ -1029,6 +1171,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel, VkFormat format,
|
||||
Bool layered, Int32 layer) {
|
||||
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
|
||||
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
@@ -1053,8 +1197,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = resource->arrayLayers;
|
||||
// A GL texture view opens onto a WINDOW of the storage's layers; a layered image
|
||||
// binding of it must not reach past that window into the parent's other layers.
|
||||
Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0);
|
||||
Uint32 layerCount = texture.IsTextureView()
|
||||
? std::min(static_cast<Uint32>(texture.GetViewNumLayers()),
|
||||
resource->arrayLayers - baseArrayLayer)
|
||||
: resource->arrayLayers;
|
||||
VkImageViewType viewType = resource->viewType;
|
||||
if (!layered) {
|
||||
switch (resource->viewType) {
|
||||
@@ -1087,7 +1236,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType;
|
||||
if (format == resource->format && isFullResourceView) {
|
||||
if (format == resource->format && isFullResourceView && !texture.IsTextureView()) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -1613,7 +1762,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool storageUpgradePending =
|
||||
!outResource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||
// Same shape for a GL texture view's demands on the image (MUTABLE_FORMAT for a
|
||||
// format-reinterpreting view, CUBE_COMPATIBLE for a cube view of an array texture):
|
||||
// nothing about the texture itself changed, but the live image cannot carry the view.
|
||||
// Masked by what this format can actually be given: MUTABLE_FORMAT is deliberately
|
||||
// withheld from formats the driver already refused it for (see SyncTextureResource), and
|
||||
// without this mask the "upgrade still pending" test below could never come true again -
|
||||
// costing every later sync of that texture the whole slow path, forever.
|
||||
VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (m_mutableFormatUnsupported.find(outResource.format) != m_mutableFormatUnsupported.end()) {
|
||||
requestedViewFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
const Bool viewFlagUpgradePending =
|
||||
(outResource.imageCreateFlags & requestedViewFlags) != requestedViewFlags;
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && !viewFlagUpgradePending &&
|
||||
outResource.syncedContentVersion == syncingContentVersion &&
|
||||
outResource.syncedShapeVersion == syncingShapeVersion &&
|
||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||
@@ -1807,6 +1969,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
// Flags a GL texture view over this storage asked for (see NoteTextureViewImageRequirements).
|
||||
// MUTABLE_FORMAT is still withheld from formats the driver has already refused it for, so a
|
||||
// reinterpreting view degrades to no view rather than to no texture.
|
||||
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (requestedViewFlags != 0) {
|
||||
imageCreateFlags |= requestedViewFlags;
|
||||
if (m_mutableFormatUnsupported.find(format) != m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
}
|
||||
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
|
||||
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
|
||||
// views - multisample sRGB render targets included.
|
||||
@@ -1967,6 +2139,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
// ...plus every format a glTextureView over this storage reinterprets it as. Those
|
||||
// are NOT enumerable from ResolveSampledImageViewFormat - an application may name any
|
||||
// member of the format's view class (GL 4.6 core table 8.21) - so without this the
|
||||
// list would forbid the very view the MUTABLE_FORMAT bit was requested for.
|
||||
AppendViewRequestedFormats(texture, viewFormats);
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
@@ -1999,12 +2176,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
// Losing 2D-array compatibility only costs framebuffer attachment of this format's
|
||||
// 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
|
||||
// the z axis; failing creation would lose the texture entirely. Recorded here (the
|
||||
// per-format set below) so later syncs neither reprobe nor flag-mismatch against this
|
||||
// image and recreate it, and so GetOrCreateAttachmentViewAtMipLevel declines rather
|
||||
// than handing back a view that cannot exist - the render-pass builder then turns
|
||||
// that decline into a skipped draw instead of a null VkImageView in pAttachments.
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
"textureId=%d; creating without it (per-slice and layered framebuffer "
|
||||
"attachment of 3D textures in this format will be unavailable)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
m_2dArrayCompatibleUnsupported.insert(format);
|
||||
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
@@ -2392,6 +2573,164 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
|
||||
}
|
||||
|
||||
MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf(
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
const auto& storageOwner = texture.GetViewStorageOwner();
|
||||
return storageOwner ? *storageOwner : texture;
|
||||
}
|
||||
|
||||
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
|
||||
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
|
||||
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
|
||||
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
|
||||
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
|
||||
VkImageViewType storageViewType) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return VK_IMAGE_VIEW_TYPE_1D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
default:
|
||||
return storageViewType;
|
||||
}
|
||||
}
|
||||
|
||||
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
|
||||
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
|
||||
TextureViewWindow window{};
|
||||
window.format = resource.format;
|
||||
window.viewType = resource.viewType;
|
||||
window.baseArrayLayer = 0;
|
||||
window.layerCount = resource.arrayLayers;
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||
window.components = ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()));
|
||||
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
|
||||
if (!texture.IsTextureView()) {
|
||||
return window;
|
||||
}
|
||||
|
||||
window.isTextureView = true;
|
||||
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
|
||||
// view, so ResolveViewMipRange above already clamped them against the view's own level
|
||||
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
|
||||
// into the storage image's numbering.
|
||||
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
|
||||
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
|
||||
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
|
||||
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
|
||||
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED) {
|
||||
window.format = viewFormat;
|
||||
}
|
||||
// Recomputed against the view's own format: a depth/stencil storage viewed as
|
||||
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
|
||||
// one parameter Better Clouds deliberately sets differently on the two names.
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
|
||||
? GetAspectMaskForFormat(window.format)
|
||||
: resource.aspect,
|
||||
texture.GetDepthStencilTextureMode());
|
||||
|
||||
// Clamp to what the image actually has; a malformed view must degrade to an empty range
|
||||
// rather than reach vkCreateImageView with an out-of-bounds subresource.
|
||||
if (window.baseMipLevel >= resource.mipLevels) {
|
||||
window.baseMipLevel = resource.mipLevels - 1;
|
||||
window.levelCount = 1;
|
||||
} else {
|
||||
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
|
||||
}
|
||||
if (window.levelCount == 0) window.levelCount = 1;
|
||||
if (window.baseArrayLayer >= resource.arrayLayers) {
|
||||
window.baseArrayLayer = resource.arrayLayers - 1;
|
||||
window.layerCount = 1;
|
||||
} else {
|
||||
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
|
||||
}
|
||||
if (window.layerCount == 0) window.layerCount = 1;
|
||||
return window;
|
||||
}
|
||||
|
||||
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
|
||||
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
|
||||
// resolve of a view already creates - or recreates and copies forward - an image the view can
|
||||
// legally be built over, instead of handing back VK_NULL_HANDLE for a frame.
|
||||
void VkTextureManager::NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture) {
|
||||
const TextureIdentity storageIdentity = MakeTextureIdentity(&storageTexture);
|
||||
VkImageCreateFlags required = 0;
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(viewTexture.GetFormat()).format;
|
||||
const VkFormat storageFormat = ResolveTextureFormatInfo(storageTexture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED && storageFormat != VK_FORMAT_UNDEFINED &&
|
||||
viewFormat != storageFormat) {
|
||||
required |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
// The image may be created with a NARROWED format list (see SyncTextureResource), and
|
||||
// that list is a promise about every format the image will ever be viewed as. Record
|
||||
// this one so the promise stays true.
|
||||
m_viewRequestedFormats[storageIdentity].insert(viewFormat);
|
||||
}
|
||||
const TextureTarget viewTarget = viewTexture.GetTarget();
|
||||
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
|
||||
// Only when the storage could legally carry the bit. VK_IMAGE_CREATE_CUBE_COMPATIBLE
|
||||
// demands a 2D image with square levels and at least six array layers
|
||||
// (VUID-VkImageCreateInfo-flags-00954), and asking for it on a storage that has fewer
|
||||
// would fail vkCreateImage - which, because SyncTextureResource has already released
|
||||
// the old resource by then, would leave the PARENT texture with no image at all. A
|
||||
// degenerate view must not be able to destroy the texture it views; let its own view
|
||||
// creation fail instead.
|
||||
const IntVec3 storageSize = storageTexture.GetBaseSize();
|
||||
const Bool storageCanBeCube = storageSize.x() == storageSize.y() &&
|
||||
storageTexture.GetViewNumLayers() >= 6 &&
|
||||
storageTexture.GetTarget() != TextureTarget::Texture3D;
|
||||
if (storageCanBeCube) {
|
||||
required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
} else {
|
||||
MGLOG_W_ONCE("Texture view %d wants a cube view of texture %d, whose storage is %dx%d with %u "
|
||||
"layers and cannot be cube-compatible; the view will have no image view.",
|
||||
viewTexture.GetExternalIndex(), storageTexture.GetExternalIndex(), storageSize.x(),
|
||||
storageSize.y(), storageTexture.GetViewNumLayers());
|
||||
}
|
||||
}
|
||||
if (required == 0) {
|
||||
return;
|
||||
}
|
||||
VkImageCreateFlags& stored = m_viewRequestedImageFlags[storageIdentity];
|
||||
stored |= required;
|
||||
}
|
||||
|
||||
VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags(
|
||||
const MG_State::GLState::ITextureObject& storageTexture) const {
|
||||
const auto it = m_viewRequestedImageFlags.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
return it == m_viewRequestedImageFlags.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
void VkTextureManager::AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const {
|
||||
const auto it = m_viewRequestedFormats.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
if (it == m_viewRequestedFormats.end()) {
|
||||
return;
|
||||
}
|
||||
for (const VkFormat viewFormat : it->second) {
|
||||
if (std::find(outFormats.begin(), outFormats.end(), viewFormat) == outFormats.end()) {
|
||||
outFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
|
||||
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -22,6 +24,31 @@ class ITextureObject;
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
|
||||
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
|
||||
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
|
||||
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
|
||||
// because Vulkan devices rarely support the 3-channel layouts.
|
||||
//
|
||||
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
|
||||
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
|
||||
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
|
||||
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
|
||||
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
|
||||
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
|
||||
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
|
||||
// `.format` and ignore the rest.
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
|
||||
|
||||
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
||||
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
||||
@@ -41,6 +68,58 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
|
||||
return glTexelSize;
|
||||
}
|
||||
|
||||
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
|
||||
//
|
||||
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
|
||||
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
|
||||
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
|
||||
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
|
||||
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
|
||||
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
|
||||
// depending only on which path happens to consume it first - and the materialise path then POPS
|
||||
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
|
||||
// that split gets introduced; keep them the same function.
|
||||
//
|
||||
// Two shapes make this more than `size.z()`:
|
||||
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
|
||||
// just above), so z reads 1 and every layer above the first was silently dropped.
|
||||
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
|
||||
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
|
||||
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
|
||||
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
|
||||
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsLayered()) {
|
||||
return 1u;
|
||||
}
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
if (target == TextureTarget::TextureCubeMap) {
|
||||
return 6u;
|
||||
}
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
|
||||
// storage texture's (VkTextureManager::StorageTextureOf).
|
||||
//
|
||||
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
|
||||
// GetOrCreate*View entry point below expects values that have already been through here, and so
|
||||
// does everything that reads or copies an attachment directly. Both are identity on a plain
|
||||
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
|
||||
// and there is no second, view-only code path to keep in step.
|
||||
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
|
||||
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
|
||||
}
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
@@ -139,17 +218,35 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
|
||||
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
|
||||
// through two GL names in one draw, the parent with the stencil aspect and the view with
|
||||
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
|
||||
// same image. Without these two fields those views would alias each other in the cache.
|
||||
struct SampledImageViewKey {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
|
||||
// same window but different swizzles are different views. Baked into the key because
|
||||
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
|
||||
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
|
||||
// version moves, nothing else would ever notice a swizzle change on a view.
|
||||
Uint32 componentSwizzle = 0;
|
||||
|
||||
Bool operator==(const SampledImageViewKey& other) const {
|
||||
return baseMipLevel == other.baseMipLevel &&
|
||||
levelCount == other.levelCount &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format;
|
||||
format == other.format &&
|
||||
aspect == other.aspect &&
|
||||
componentSwizzle == other.componentSwizzle;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,10 +254,15 @@ public:
|
||||
SizeT operator()(const SampledImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -358,6 +460,58 @@ public:
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
|
||||
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
|
||||
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
|
||||
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
|
||||
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
|
||||
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
|
||||
// layout field, so a second resource aliasing the same image would desynchronise the moment
|
||||
// either of them transitioned it.
|
||||
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// The window a GL texture object opens onto its storage image. For a plain texture this is
|
||||
// the resource's own full extent; for a view it is the sub-range, format and aspect
|
||||
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
|
||||
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
|
||||
// the storage texture's own defaults.
|
||||
struct TextureViewWindow {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
|
||||
VK_COMPONENT_SWIZZLE_A};
|
||||
Bool isTextureView = false;
|
||||
};
|
||||
|
||||
// The four component swizzles packed into one value, for the sampled-view cache key.
|
||||
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
|
||||
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
|
||||
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
|
||||
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
|
||||
}
|
||||
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
|
||||
const TextureResource& resource) const;
|
||||
// Records what a GL texture view needs of the image it views, so the next sync of the
|
||||
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
|
||||
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
|
||||
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture);
|
||||
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
|
||||
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with.
|
||||
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const;
|
||||
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
|
||||
// image. Shared back end of every GL-texture-view sampled path.
|
||||
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource, const TextureViewWindow& window);
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
@@ -572,6 +726,19 @@ private:
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
|
||||
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
|
||||
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
|
||||
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
|
||||
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
|
||||
// feature almost none of them use. A SAME-format view - which is the common case, and the
|
||||
// Better Clouds case - needs no flag at all and therefore costs nothing.
|
||||
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
|
||||
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
|
||||
// that NO other format will ever be viewed, and building a view outside it is
|
||||
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
|
||||
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
@@ -563,7 +564,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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.
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
|
||||
Uint32 m_nativeSubgroupSize = 0;
|
||||
Bool m_nativeSubgroupSupported = false;
|
||||
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||
@@ -584,6 +585,30 @@ 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;
|
||||
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
|
||||
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
|
||||
// and capturable by name through transform feedback). Cached at device creation and
|
||||
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
|
||||
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
|
||||
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
|
||||
// that is invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
|
||||
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
|
||||
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
|
||||
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
|
||||
// colour outside the palette has to be snapped to the nearest predefined one. Both features
|
||||
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
|
||||
// colour without naming the image format it will be paired with, which GL's sampler objects
|
||||
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
|
||||
// to be able to fall back to the snapped value once it is reached.
|
||||
Bool m_customBorderColorFeatureEnabled = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
|
||||
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
|
||||
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
|
||||
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
|
||||
Bool m_sampleRateShadingFeatureEnabled = 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
|
||||
@@ -693,15 +718,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
|
||||
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
|
||||
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
|
||||
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
|
||||
// finds that defect with a working control - or
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
|
||||
// GENERATED count through this pool instead, whose type the arming picks:
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
|
||||
// query with its rasterizer-discard feature (exact semantics by definition -
|
||||
// the extension exists because GL needs this count without a capture), else a
|
||||
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
|
||||
// per primitive reaching primitive clipping - after every vertex processing
|
||||
// stage, before rasterizer discard - which is the same set).
|
||||
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
|
||||
// every draw with no open capture - a PAUSED span's draws included - takes a
|
||||
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
|
||||
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
|
||||
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
|
||||
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
|
||||
// GL query span may therefore hold slots of both pools.
|
||||
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
|
||||
// VK_EXT_primitives_generated_query: base feature, and the
|
||||
// ...WithRasterizerDiscard feature without which a discarding draw inside the
|
||||
// query is invalid usage (so the reroute never picks the dedicated pool on a
|
||||
// base-only device - GL applications toggle discard freely).
|
||||
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||
// tessellationShader was enabled at device creation (it is taken whenever the
|
||||
// device advertises it); gates the probe's PATCHES shape.
|
||||
Bool m_tessellationShaderFeatureEnabled = false;
|
||||
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
|
||||
MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||
// The bring-up probe measured this device's stream query as counting draws made
|
||||
// with no capture span open (the StreamCounts verdict) - so it counts the
|
||||
// PAUSED-span ones too, through the stream slot they take when nothing is
|
||||
// rerouted. Only the probe can know this, so it stays false wherever the probe
|
||||
// is not consulted (the forced arms), which keeps those lanes' accounting as it
|
||||
// was.
|
||||
Bool m_primGenStreamCountsXfbInactiveDraws = false;
|
||||
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
|
||||
Uint32 m_primGenRerouteSlotCursor = 0;
|
||||
Vector<Uint32> m_primGenRerouteActiveSlots;
|
||||
Bool m_primGenRerouteSlotOpen = false;
|
||||
Uint32 m_primGenRerouteOpenSlot = 0;
|
||||
// Runs the bring-up probe (memoized per process) and decides
|
||||
// m_primGenRerouteKind. Called at the end of device creation: it records on
|
||||
// m_graphicsQueue, which nothing else is using yet.
|
||||
void ArmPrimGenReroute();
|
||||
|
||||
public:
|
||||
// Whether a GENERATED span opened now will have the draws made while the GL
|
||||
// span is PAUSED counted on the GPU - through the reroute pool, which takes
|
||||
// every draw with no open capture, or (where the reroute is not armed because
|
||||
// the stream query was measured to count capture-less draws) through the stream
|
||||
// slot such a draw still takes. The frontend's CPU paused-primitive counter
|
||||
// must not be added on top of either: it would double count, and it cannot
|
||||
// price the draws that matter anyway - only 3 of the ~15 draw entry points
|
||||
// write it and it answers 0 for GL_PATCHES. Read once per span, after
|
||||
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
|
||||
Bool ArePausedDrawsGpuCounted() const;
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||
Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
@@ -733,6 +817,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||
// index type, neither of which the mode/program/state hashes carry. Without it an
|
||||
// indexed and a non-indexed draw over the same program and state collide on one entry
|
||||
// and the second one gets the first one's restart setting.
|
||||
Bool primitiveRestartEnable = false;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
@@ -746,9 +836,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const;
|
||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||
// and the pipeline-state memo word so the two cannot disagree.
|
||||
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||
// it (the effective sample mask), so a draw that changes only the target's sample count
|
||||
// has to recompute rather than reuse.
|
||||
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Uint64 m_pipelineStateHash = 0;
|
||||
Bool m_pipelineStateHashValid = false;
|
||||
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||
@@ -790,7 +889,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
|
||||
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
|
||||
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
|
||||
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
|
||||
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
|
||||
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
|
||||
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
|
||||
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
|
||||
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
|
||||
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
|
||||
@@ -801,6 +902,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_lastSampledSetProgramVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
Uint64 m_lastSampledSetSamplingGeneration = 0;
|
||||
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
|
||||
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
|
||||
// VkProgramObject::xfbCaptureDeclined.
|
||||
Bool m_currentDrawXfbCaptureDeclined = false;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
@@ -865,6 +971,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
|
||||
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
|
||||
// already separates indexed from non-indexed draws, but not one index TYPE from
|
||||
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
|
||||
// makes those two draws want different pipelines.
|
||||
Bool primitiveRestartEnable = false;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
@@ -893,6 +1005,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// probe the pipeline memo after a state change without re-fetching the
|
||||
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||
Uint32 renderPassColorCount = 0;
|
||||
// Pinned with the colour count and for the same reason: the fast path recomputes the
|
||||
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
|
||||
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||
@@ -1168,13 +1283,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CreateSwapchain();
|
||||
void CreateCommandPool();
|
||||
|
||||
// Whether THIS draw's primitive stream restarts, and therefore what
|
||||
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
|
||||
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
|
||||
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
|
||||
// and the index TYPE (an application restart index that does not fit the type matches no
|
||||
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
|
||||
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
||||
const IndexBufferView* pIndexBufferView) const;
|
||||
|
||||
VkPipeline GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
@@ -1241,13 +1366,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLenum filter);
|
||||
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
|
||||
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
|
||||
// Clears one layer of a colour image through a throwaway render pass whose entire content
|
||||
// is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
|
||||
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
|
||||
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
|
||||
// layout the caller already tracks for the whole image, so this never has to touch
|
||||
// resource->layout.
|
||||
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue);
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
// The multisample arm of the above. Split out rather than branched inline because it
|
||||
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
|
||||
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
|
||||
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
const Vector<PendingClearEntry>& pendingClears);
|
||||
Bool MaterializePendingClearForRenderbuffer(
|
||||
VkCommandBuffer commandBuffer,
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
|
||||
@@ -39,18 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||
!MG_Config::Features.DisableSubgroup;
|
||||
!MG_Config::Features.MagmaDisableSubgroup;
|
||||
}
|
||||
|
||||
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 !=
|
||||
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
|
||||
MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPBarrier() {
|
||||
return MG_Config::Features.IterationRPFixBarrier;
|
||||
return MG_Config::Features.MagmaIterationRPFixBarrier;
|
||||
}
|
||||
|
||||
inline Bool ShouldDeriveNumSubgroups() {
|
||||
@@ -58,6 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Debug.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
|
||||
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
|
||||
// off the query and then trips a different limit here would have no way to explain it.
|
||||
constexpr SizeT kMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLsizei kMaxDebugMessageLength = 1024;
|
||||
constexpr GLsizei kMaxLabelLength = 256;
|
||||
|
||||
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
|
||||
// nothing else in MobileGL reads it, and it is keyed on the context id so a
|
||||
// destroyed-and-recreated context starts with an empty stack and no labels - which the
|
||||
// unit tests, which recreate the context between cases, depend on.
|
||||
struct DebugState {
|
||||
Uint64 contextId = 0;
|
||||
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
|
||||
// the context with is implicit and is what makes the reported depth start at 1.
|
||||
Vector<String> groupStack;
|
||||
// Keyed by (identifier, name); see MakeObjectLabelKey.
|
||||
UnorderedMap<Uint64, String> objectLabels;
|
||||
};
|
||||
|
||||
DebugState& State() {
|
||||
static DebugState state;
|
||||
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
|
||||
if (state.contextId != contextId) {
|
||||
state.contextId = contextId;
|
||||
state.groupStack.clear();
|
||||
state.objectLabels.clear();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
|
||||
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
|
||||
}
|
||||
|
||||
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
|
||||
}
|
||||
|
||||
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
|
||||
// are reserved for the implementation itself.
|
||||
Bool ValidateInjectedSource(GLenum source, const char* caller) {
|
||||
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidEnum, caller,
|
||||
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
|
||||
"GL_DEBUG_SOURCE_THIRD_PARTY.",
|
||||
MG_Util::ConvertGLEnumToString(source)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
|
||||
// every one of these entry points spells "just use the whole thing".
|
||||
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
|
||||
const char* what) {
|
||||
const GLsizei effective =
|
||||
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
|
||||
if (effective < limit) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidValue, caller,
|
||||
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
|
||||
limit));
|
||||
return false;
|
||||
}
|
||||
|
||||
String MakeDebugString(GLsizei length, const GLchar* text) {
|
||||
if (text == nullptr) return {};
|
||||
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
|
||||
}
|
||||
|
||||
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
|
||||
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
|
||||
// has a frontend name check - so this is answered exactly rather than waved through.
|
||||
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
|
||||
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
|
||||
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
|
||||
outIdentifierKnown = true;
|
||||
auto* context = MG_State::pGLContext.get();
|
||||
switch (identifier) {
|
||||
case GL_BUFFER:
|
||||
return context->ValidateBufferName(name);
|
||||
case GL_SHADER:
|
||||
return context->ValidateShaderName(name);
|
||||
case GL_PROGRAM:
|
||||
return context->ValidateProgramName(name);
|
||||
case GL_VERTEX_ARRAY:
|
||||
return context->ValidateVertexArrayName(name);
|
||||
case GL_QUERY:
|
||||
return IsQuery(name) == GL_TRUE;
|
||||
case GL_PROGRAM_PIPELINE:
|
||||
return context->ValidateProgramPipelineName(name);
|
||||
case GL_TRANSFORM_FEEDBACK:
|
||||
return context->ValidateTransformFeedbackName(name);
|
||||
case GL_SAMPLER:
|
||||
return context->ValidateSamplerName(name);
|
||||
case GL_TEXTURE:
|
||||
return context->ValidateTextureName(name);
|
||||
case GL_RENDERBUFFER:
|
||||
return context->ValidateRenderbufferName(name);
|
||||
case GL_FRAMEBUFFER:
|
||||
// Name 0 is the default framebuffer, which is a real, labellable object.
|
||||
return name == 0 || context->ValidateFramebufferName(name);
|
||||
default:
|
||||
outIdentifierKnown = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetDebugGroupStackDepth() {
|
||||
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
|
||||
// reported depth is one more than the number of pushes the application has made.
|
||||
return static_cast<GLint>(State().groupStack.size()) + 1;
|
||||
}
|
||||
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
auto& state = State();
|
||||
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
|
||||
// Not INVALID_*: KHR_debug gives the group stack its own error code.
|
||||
RecordDebugError(ErrorCode::StackOverflow, __func__,
|
||||
std::format("the debug group stack is already {} deep, which is its maximum.",
|
||||
kMaxDebugGroupStackDepth));
|
||||
return;
|
||||
}
|
||||
state.groupStack.push_back(MakeDebugString(length, message));
|
||||
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
|
||||
}
|
||||
|
||||
void PopDebugGroup() {
|
||||
auto& state = State();
|
||||
if (state.groupStack.empty()) {
|
||||
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
|
||||
RecordDebugError(ErrorCode::StackUnderflow, __func__,
|
||||
"the debug group stack holds only the group the context was created with.");
|
||||
return;
|
||||
}
|
||||
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
|
||||
state.groupStack.pop_back();
|
||||
}
|
||||
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
switch (type) {
|
||||
case GL_DEBUG_TYPE_ERROR:
|
||||
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_PORTABILITY:
|
||||
case GL_DEBUG_TYPE_PERFORMANCE:
|
||||
case GL_DEBUG_TYPE_MARKER:
|
||||
case GL_DEBUG_TYPE_PUSH_GROUP:
|
||||
case GL_DEBUG_TYPE_POP_GROUP:
|
||||
case GL_DEBUG_TYPE_OTHER:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("type {} is not a debug message type.",
|
||||
MG_Util::ConvertGLEnumToString(type)));
|
||||
return;
|
||||
}
|
||||
switch (severity) {
|
||||
case GL_DEBUG_SEVERITY_HIGH:
|
||||
case GL_DEBUG_SEVERITY_MEDIUM:
|
||||
case GL_DEBUG_SEVERITY_LOW:
|
||||
case GL_DEBUG_SEVERITY_NOTIFICATION:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("severity {} is not a debug message severity.",
|
||||
MG_Util::ConvertGLEnumToString(severity)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
// No callback is ever invoked and the message log is empty by construction
|
||||
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
|
||||
// application-visible effect is exactly the error checking above. The text still reaches
|
||||
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
|
||||
// level, so an application that inserts a message per draw costs nothing in a release build.
|
||||
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
|
||||
}
|
||||
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
|
||||
|
||||
auto& labels = State().objectLabels;
|
||||
const Uint64 key = MakeObjectLabelKey(identifier, name);
|
||||
if (label == nullptr) {
|
||||
// GL 4.6 core 20.5: a NULL label removes any label the object had.
|
||||
labels.erase(key);
|
||||
return;
|
||||
}
|
||||
labels[key] = MakeDebugString(length, label);
|
||||
}
|
||||
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
if (bufSize < 0) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
|
||||
return;
|
||||
}
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& labels = State().objectLabels;
|
||||
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
|
||||
const String& text = it != labels.end() ? it->second : String{};
|
||||
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
|
||||
// back an empty string with length 0 rather than an error.
|
||||
SizeT copied = 0;
|
||||
if (label != nullptr && bufSize > 0) {
|
||||
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
|
||||
std::memcpy(label, text.data(), copied);
|
||||
label[copied] = '\0';
|
||||
}
|
||||
if (length != nullptr) {
|
||||
*length = static_cast<GLsizei>(copied);
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
|
||||
// and to name their objects; Better Clouds calls all four for exactly that.
|
||||
//
|
||||
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
|
||||
// the host driver. Two independent reasons:
|
||||
//
|
||||
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
|
||||
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
|
||||
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
|
||||
// labelling.
|
||||
// * A debug GROUP is only meaningful if it brackets the commands the application issued
|
||||
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
|
||||
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
|
||||
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
|
||||
// wrong commands, which is a misleading capture rather than a helpful one.
|
||||
//
|
||||
// What the application can rely on is the observable contract: the group stack depth is real
|
||||
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
|
||||
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
|
||||
void PopDebugGroup();
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf);
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
|
||||
|
||||
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
|
||||
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
|
||||
GLint GetDebugGroupStackDepth();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -34,8 +34,81 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
|
||||
// Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
|
||||
// is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
|
||||
// backend samples during this draw describes the program it is drawing), so the draw funnel
|
||||
// below resolves it exactly once and hands it to both users.
|
||||
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const char* functionName) {
|
||||
// "If there is no current program object or bound program pipeline object, the results of
|
||||
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
|
||||
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
|
||||
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
|
||||
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
|
||||
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
|
||||
// the compute shader stage"), which is why this lives on the draw path and not in the
|
||||
// shared ValidateProgramForExecution below.
|
||||
if (!currentProgram) return false;
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
|
||||
// inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
|
||||
// evaluation or geometry stage has an executable but no program supplies an executable
|
||||
// VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
|
||||
// shape - so in practice it catches a program pipeline assembled out of stage programs,
|
||||
// which today draws happily and renders nothing.
|
||||
//
|
||||
// Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
|
||||
// program is the graphics composite, whose linked-shader snapshot is built out of exactly
|
||||
// the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
|
||||
// stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
|
||||
// deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
|
||||
// nothing else here should start refusing draws GL accepts.
|
||||
//
|
||||
// On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
|
||||
// which shares that helper and legitimately has no vertex stage - is untouched.
|
||||
const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
||||
if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
|
||||
// default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
|
||||
// for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
|
||||
// drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
|
||||
// rules out baking it at link time.
|
||||
//
|
||||
// Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
|
||||
// decides the value and either half can move between draws. It costs a phase-A flag read for
|
||||
// every program that has no shim, and a 4-byte compare for the ones that do: the write only
|
||||
// bumps the UBO content version when the number actually changes, so a run of draws into one
|
||||
// framebuffer re-uploads nothing.
|
||||
static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
||||
if (!program || !program->UsesReservedNumSamples()) return;
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
|
||||
// when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
|
||||
}
|
||||
|
||||
// The one funnel every drawing command passes through. Order is load-bearing: validate first
|
||||
// (a rejected draw must leave state alone), then publish the sample count - which reads the
|
||||
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
|
||||
// and before the backend consumes the program's UBO content version.
|
||||
static Bool PrepareCurrentProgramForDraw(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
|
||||
PublishDrawFramebufferSampleCount(currentProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
|
||||
@@ -73,6 +146,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
|
||||
// Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
|
||||
// them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
|
||||
// line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
|
||||
// the default arm did - made AccountTransformFeedbackPrimitives bail before it had
|
||||
// recorded anything, so an adjacency capture advanced neither the captured-vertex
|
||||
// counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
|
||||
// that routes the transform feedback queries to the driver's own counter.
|
||||
case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
|
||||
case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
|
||||
case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
|
||||
// GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
|
||||
// on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
// to the driver's own counter, which is the only correct source for a patch capture.
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -99,11 +186,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_LINES:
|
||||
case GL_LINE_STRIP:
|
||||
case GL_LINE_LOOP:
|
||||
// An adjacency primitive delivers the same line/triangle to the geometry stage; the
|
||||
// adjacent vertices are context, not part of the primitive.
|
||||
case GL_LINES_ADJACENCY:
|
||||
case GL_LINE_STRIP_ADJACENCY:
|
||||
verticesPerPrimitive = 2;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN:
|
||||
case GL_TRIANGLES_ADJACENCY:
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||
verticesPerPrimitive = 3;
|
||||
break;
|
||||
default:
|
||||
@@ -308,11 +401,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POINTS:
|
||||
compatible = mode == GL_POINTS;
|
||||
break;
|
||||
// The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
|
||||
// This arm is only reached when the program has NO geometry or tessellation
|
||||
// evaluation stage, and without a geometry stage the adjacent vertices are simply
|
||||
// ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
|
||||
// so the combination is legal and must capture. Omitting them raised a spurious
|
||||
// GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
|
||||
// with its pre-draw bytes. The geometry-stage input table above already carries the
|
||||
// same four arms; this is the second table catching up with it.
|
||||
case GL_LINES:
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
|
||||
mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
|
||||
mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -382,6 +485,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an
|
||||
// INVALID_VALUE error is generated". The same sentence covers instancecount and the
|
||||
// MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the
|
||||
// caller so the message says which argument the application actually got wrong.
|
||||
static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) {
|
||||
if (value >= 0) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(argumentName) + " must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is
|
||||
// generated". Both are uints, so a caller that passes -1 for start arrives here as
|
||||
// 0xFFFFFFFF and is caught by the same comparison - which is exactly what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks.
|
||||
static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) {
|
||||
if (end >= start) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "end must not be less than start."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
||||
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
||||
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
||||
@@ -688,7 +816,65 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
|
||||
// NO control stage tessellates at; both backends have to synthesize that control stage
|
||||
// themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
|
||||
// there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
|
||||
// pname is the only error the spec lists: any float values are accepted, negatives and NaN
|
||||
// included, and it is the tessellator that clamps them.
|
||||
//
|
||||
// This used to be a stub, which is why the two synthesizers hardcoded 1.0.
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values) {
|
||||
if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
|
||||
return;
|
||||
}
|
||||
if (!values) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
|
||||
return;
|
||||
}
|
||||
if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
|
||||
MG_State::pGLContext->SetPatchDefaultOuterLevel(
|
||||
FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else {
|
||||
MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
|
||||
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
|
||||
// GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
|
||||
// Forwarding an undefined bit to the host driver let a caller that had computed its mask
|
||||
// wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
|
||||
constexpr GLbitfield kAllDefinedBarrierBits =
|
||||
GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
|
||||
GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
|
||||
GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
|
||||
GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
|
||||
GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;
|
||||
|
||||
Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
|
||||
if (barriers == GL_ALL_BARRIER_BITS) return true;
|
||||
if ((barriers & ~kAllDefinedBarrierBits) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
|
||||
"barriers contains bits that are not defined barrier bits."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MemoryBarrier(GLbitfield barriers) {
|
||||
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
|
||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||
if (!memoryBarrier) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -699,7 +885,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
void TextureBarrier() {
|
||||
// GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
|
||||
// framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
|
||||
// read texels of a texture that is also attached to the current framebuffer.
|
||||
//
|
||||
// Both backends serve this through their existing memory-barrier hook rather than a new
|
||||
// entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
|
||||
// writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
|
||||
// is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
|
||||
// scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
|
||||
// subset, so it cannot under-synchronize.
|
||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||
if (!memoryBarrier) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
}
|
||||
|
||||
void MemoryBarrierByRegion(GLbitfield barriers) {
|
||||
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
|
||||
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
|
||||
if (!memoryBarrierByRegion) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -713,14 +921,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||
}
|
||||
@@ -798,7 +1006,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
|
||||
// they produce for a call made with no program bound. Same precedence decision, and
|
||||
// the same reason, as DispatchComputeIndirect above.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||
if (!multiDrawElementsIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -819,7 +1027,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
// See MultiDrawElementsIndirectCount, including why this one goes last.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||
if (!multiDrawArraysIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -834,14 +1042,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateDrawElementsRange(__func__, start, end)) return;
|
||||
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
||||
}
|
||||
@@ -849,7 +1060,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||
baseinstance);
|
||||
@@ -858,29 +1069,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return;
|
||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
|
||||
@@ -890,21 +1104,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||
DrawArraysIndirect_Backend(mode, indirect);
|
||||
@@ -912,15 +1126,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
@@ -928,7 +1144,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (drawcount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -942,7 +1158,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||
}
|
||||
@@ -950,8 +1166,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
|
||||
// GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate
|
||||
// DrawElementsBaseVertex calls, so each element of the count array carries the same
|
||||
// non-negative requirement the single-draw entry point applies to its own count. The
|
||||
// whole call is rejected before any sub-draw is issued, which is what makes the error
|
||||
// observable at all - a driver that drew the valid prefix first would leave the
|
||||
// framebuffer half-written.
|
||||
if (count != nullptr) {
|
||||
for (GLsizei draw = 0; draw < drawcount; ++draw) {
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return;
|
||||
}
|
||||
}
|
||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
@@ -961,7 +1190,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
@@ -1385,7 +1614,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// (GL 4.6 core 10.3.7).
|
||||
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
|
||||
GLsizei instancecount) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return;
|
||||
if (!PrepareCurrentProgramForDraw(functionName)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
|
||||
if (instancecount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1408,8 +1637,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(id) + " is not a transform feedback object name."));
|
||||
return;
|
||||
}
|
||||
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
|
||||
if (stream != 0) {
|
||||
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
|
||||
// answers as 1 - so stream 0 is the only one that exists and anything else is
|
||||
// INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
|
||||
// never drift: if vertex-stream support ever lands, this bound moves with the limit.
|
||||
GLint maxVertexStreams = 1;
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
@@ -1427,6 +1661,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
|
||||
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
|
||||
if (vertices == 0) return;
|
||||
const auto count = static_cast<GLsizei>(vertices);
|
||||
|
||||
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void PatchParameteri(GLenum pname, GLint value);
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
void MemoryBarrierByRegion(GLbitfield barriers);
|
||||
void TextureBarrier();
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../Framebuffer/GL_Framebuffer.h"
|
||||
#include "../VertexArray/GL_VertexArray.h"
|
||||
#include "../Sync/GL_Sync.h"
|
||||
#include "../Debug/GL_Debug.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -159,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
|
||||
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
|
||||
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
|
||||
DECLARE_GL_FUNCTION_HEAD(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) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup)
|
||||
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
(void)length;
|
||||
(void)label;
|
||||
}
|
||||
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
if (label && bufSize > 0) {
|
||||
label[0] = '\0';
|
||||
}
|
||||
}
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
|
||||
@@ -424,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
||||
@@ -936,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
||||
@@ -996,7 +983,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||
@@ -1007,7 +994,7 @@ DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||
@@ -1060,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||
@@ -1120,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
|
||||
@@ -1163,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
|
||||
@@ -1848,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
@@ -2062,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
|
||||
@@ -2559,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
|
||||
|
||||
@@ -474,6 +474,75 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
|
||||
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
|
||||
// one another in that section and a CTS case that walks the family must not get five
|
||||
// different answers - which is exactly what happened when these lived in one helper that
|
||||
// only two of the five went through.
|
||||
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>&
|
||||
framebufferObject,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
|
||||
// than or equal to MAX_COLOR_ATTACHMENTS."
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
|
||||
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
|
||||
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
|
||||
// object is always there, and framebuffer 0 has to be recognised by identity instead,
|
||||
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
|
||||
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
|
||||
// from what the swapchain keeps publishing.
|
||||
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
|
||||
if (!framebufferObject ||
|
||||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"No framebuffer object is bound to the target; the default framebuffer's attachments "
|
||||
"cannot be named."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
|
||||
// texture with immutable storage it "must be smaller than the number of levels the texture
|
||||
// has". Split from the attachment-point half because the caller only has a texture object
|
||||
// once the detach (texture == 0) case is behind it.
|
||||
Bool ValidateFramebufferTextureLevel(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLint level) {
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Texture level must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (!textureObject || !textureObject->IsImmutable()) {
|
||||
// A mutable texture has no level bound here: a level it has not specified yet is
|
||||
// not an error, it just leaves the framebuffer incomplete.
|
||||
return true;
|
||||
}
|
||||
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
|
||||
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
|
||||
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
|
||||
// two-level view onto a ten-level texture would accept level 5 and attach an image
|
||||
// nothing can draw into.
|
||||
const Uint levelBound = textureObject->GetAddressableLevelCount();
|
||||
if (static_cast<Uint>(level) >= levelBound) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
|
||||
levelBound)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level,
|
||||
TextureUploadTarget textureUploadTarget, Bool layered = false) {
|
||||
@@ -482,10 +551,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
|
||||
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
|
||||
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
|
||||
// which half is being recorded - and dropping it here (the parameter defaults to
|
||||
// false) recorded a non-layered depth/stencil attachment beside a layered colour
|
||||
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
|
||||
// own rule, and downstream it means the depth/stencil attachment covers layer 0
|
||||
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
|
||||
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
|
||||
// attached one layer of it beside a layered colour target, which the driver
|
||||
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
|
||||
// produced nothing. This is the shape
|
||||
// texture_cube_map_array.stencil_attachments_*_layered and
|
||||
// geometry_shader.layered_framebuffer.stencil_support are built on.
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -497,13 +580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -518,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -624,16 +702,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
|
||||
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||
// The multisample TEXTURE path resolves the limit per format the same way
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
|
||||
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
|
||||
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
|
||||
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
|
||||
// count it actually hands the driver.
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
|
||||
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
|
||||
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
|
||||
// integer path is genuinely one sample, and accepting four only moved the failure from an
|
||||
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
|
||||
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
|
||||
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
|
||||
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
|
||||
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
|
||||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
|
||||
return 0;
|
||||
}
|
||||
const auto& sampleCounts =
|
||||
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||
return sampleCounts.empty() ? 0 : sampleCounts.front();
|
||||
}
|
||||
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
@@ -644,13 +739,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
&normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
|
||||
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
|
||||
// supported for internalformat (see GetInternalformativ)", and
|
||||
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
|
||||
// list. It was never consulted here - the TODO that deferred it was written before the
|
||||
// query was backed and had gone stale - so a format whose multisample probes fail inside
|
||||
// a category that allows four was accepted at four, quietly allocated at one by
|
||||
// ClampSamplesToBackendSupport, and then reported as four by
|
||||
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
|
||||
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
|
||||
if (probedMaxSamples > 0) {
|
||||
return probedMaxSamples;
|
||||
}
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
|
||||
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
|
||||
// clamps to it, while the frontend honours what it advertised.
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
|
||||
return GetAdvertisedIntegerMaxSamples();
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
@@ -682,8 +788,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
|
||||
// glGetInternalformativ is backed; integer formats are handled below.
|
||||
// Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
|
||||
// to the format's category pname where nothing was probed. (This carried a TODO deferring
|
||||
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
|
||||
// both renderbuffers and multisample textures since, so the deferral was collected.)
|
||||
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
|
||||
if (samples > maxSamples) {
|
||||
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
|
||||
@@ -1048,13 +1156,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1069,6 +1171,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
if (layer < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -1191,6 +1294,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
// glFramebufferTexture2D is by far the most-used member of the family and the only one
|
||||
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
|
||||
// conditions have to be asked here explicitly.
|
||||
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
|
||||
attachmentType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1205,6 +1315,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -1241,6 +1352,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
|
||||
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
|
||||
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
|
||||
// every texture name that was never generated.
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1291,13 +1408,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State",
|
||||
"Texture level must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// The whole level condition, not just its negative half: glNamedFramebufferTexture and
|
||||
// glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
|
||||
// be rejected on both or a CTS case gets two answers for one rule.
|
||||
if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
|
||||
|
||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
|
||||
Bool layered = false;
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Getter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
@@ -92,8 +95,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// limits they advertise still have to be legal.
|
||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||
// The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
|
||||
// literal: they used to be independent (4096 components against 128 vectors, 64 varying
|
||||
// components against 8 varying vectors) and could not both be describing the same
|
||||
// capacity. Both are shared with BuildTBuiltInResource through Types.h, because
|
||||
// gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
|
||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxVertexOutputComponents = 64;
|
||||
constexpr GLint kFrontendMaxFragmentInputComponents = 128;
|
||||
@@ -105,21 +115,61 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
|
||||
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
|
||||
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks +
|
||||
kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks;
|
||||
constexpr GLint kFrontendMaxVaryingComponents = 64;
|
||||
constexpr GLint kFrontendMaxVaryingVectors = 8;
|
||||
// ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
|
||||
// gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
|
||||
// answer only; 32 is the GL 4.6 core minimum (table 23.57).
|
||||
constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
|
||||
constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
|
||||
// The compute stage's share of the combined sum below. Compute's own per-stage answer is
|
||||
// backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not
|
||||
// what that query returns - it is the GL 4.3 core minimum, present here only so the
|
||||
// combined total covers all SIX stages.
|
||||
constexpr GLint kFrontendMaxComputeUniformBlocksShare = 14;
|
||||
// GL 4.6 table 23.64 orders MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >=
|
||||
// every per-stage count, and the sum has to run over SIX stages, not three and not five.
|
||||
// Three (42) was the original bug. Five (70) replaced it and broke the middle term the
|
||||
// other way: compute's per-stage count is backend-derived and clamps at the binding count,
|
||||
// so a device reporting descriptor-indexing-scale uniform buffers (Adreno reports
|
||||
// maxPerStageDescriptorUniformBuffers = 16777216) advertised 84 compute blocks against a
|
||||
// combined 70. Six stages x 14 = 84, which is also exactly the binding-point count and the
|
||||
// arithmetic the GL 4.5 minimum of 84 bindings is built from, so the ordering is now tight
|
||||
// rather than accidental.
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks =
|
||||
kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
|
||||
kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks + kFrontendMaxComputeUniformBlocksShare;
|
||||
constexpr GLint kFrontendMaxVaryingComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVaryingVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
|
||||
constexpr GLint kFrontendMaxProgramTexelOffset = 7;
|
||||
constexpr GLint kFrontendMinProgramTexelOffset = -8;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
|
||||
// ARB_transform_feedback3's vertex-stream count. One is what this implementation can
|
||||
// actually emit to; see the GL_MAX_VERTEX_STREAMS case for why it is not four.
|
||||
constexpr GLint kFrontendMaxVertexStreams = 1;
|
||||
constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
|
||||
constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 36;
|
||||
// GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
|
||||
// how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
|
||||
// GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
|
||||
// the ceiling here.
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 84;
|
||||
constexpr GLint kFrontendSubpixelBits = 4;
|
||||
constexpr GLint kFrontendMaxSamples = 4;
|
||||
constexpr GLint kFrontendMaxSamples =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
|
||||
// ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
|
||||
// glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
|
||||
// plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
|
||||
// suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
|
||||
// caller reading its own uninitialised stack slot and strands an error for the next
|
||||
// unrelated call to trip over. The extension is deliberately NOT advertised, so the
|
||||
// numbers are a table entry, not a capability claim.
|
||||
constexpr GLint kFrontendMaxSubroutines = 256;
|
||||
constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;
|
||||
|
||||
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
||||
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
||||
@@ -133,9 +183,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.64: components + blocks * (blockSize / 4). The product has to be
|
||||
// formed in 64 bits and saturated on the way out - it overflowed a signed 32-bit int on
|
||||
// every Vulkan host that reports a large maxUniformBufferRange. A Mali driver answering
|
||||
// 0xFFFFFFFF saturates to INT32_MAX in the loader, and 14 * (2147483647 / 4) + 4096 wraps
|
||||
// to -1073737742, which the conformance suite read back as a limit "smaller than 58368".
|
||||
// Saturating instead of wrapping is also the only honest answer: an implementation that
|
||||
// can serve more components than a GLint holds still has to report a GLint.
|
||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||
GLint maxUniformBlockSizeBytes) {
|
||||
return maxDefaultUniformComponents + maxUniformBlocks * (maxUniformBlockSizeBytes / 4);
|
||||
const Int64 blocks = std::max<Int64>(static_cast<Int64>(maxUniformBlocks), 0);
|
||||
const Int64 componentsPerBlock = std::max<Int64>(static_cast<Int64>(maxUniformBlockSizeBytes), 0) / 4;
|
||||
const Int64 total = static_cast<Int64>(maxDefaultUniformComponents) + blocks * componentsPerBlock;
|
||||
return static_cast<GLint>(std::min<Int64>(total, std::numeric_limits<GLint>::max()));
|
||||
}
|
||||
|
||||
bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) {
|
||||
@@ -303,24 +363,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -472,10 +514,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
||||
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
||||
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
||||
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
||||
// just told the application to use. The backends clamp the realised count instead.
|
||||
// before it is advertised. gl_MaxSamples expands from the same floored number
|
||||
// (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
|
||||
//
|
||||
// THE FLOOR STOPS HERE, and that is the point. It used to be applied to
|
||||
// GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
|
||||
// on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
|
||||
// every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
|
||||
// reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
|
||||
// single sample, so flooring the query at 4 did not make four samples exist - it made the
|
||||
// backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
|
||||
// per-sample data it could never read back. Reporting what was probed turns that into an
|
||||
// honest "unsupported" the application can branch on.
|
||||
GLint GetAdvertisedMaxSamples() {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return kFrontendMaxSamples;
|
||||
@@ -483,6 +533,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
|
||||
// see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
|
||||
// floored rather than trusted.
|
||||
namespace {
|
||||
GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetAdvertisedColorTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedDepthTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedIntegerMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
|
||||
}
|
||||
|
||||
// Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
|
||||
// gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
const GLubyte* GetString(GLenum name) {
|
||||
static String vendorString;
|
||||
@@ -679,12 +773,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
// Same reason as the three above: the integer fallback would round the fraction to 0
|
||||
// or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE.
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
// Float-native state, so GL 4.6 core 2.2.2's "zero becomes FALSE, every other value
|
||||
// becomes TRUE" has to be applied to the VALUE. Answering these through the integer getter
|
||||
// below instead - which rounds - reported GL_FALSE for a perfectly non-zero level of 0.25,
|
||||
// and every other float state in this function already reads through GetFloatv for exactly
|
||||
// that reason.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const GLsizei componentCount = pname == GL_PATCH_DEFAULT_OUTER_LEVEL ? 4 : 2;
|
||||
GLfloat levels[4] = {};
|
||||
GetFloatv(pname, levels);
|
||||
for (GLsizei i = 0; i < componentCount; ++i) {
|
||||
params[i] = levels[i] != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -734,6 +846,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = depthRange.y();
|
||||
return;
|
||||
}
|
||||
// glPatchParameterfv's two states. Float-native, so they are answered here rather than
|
||||
// through the integer fallback below - which rounds, and would report 0 for a level of 0.5.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
params[0] = outer.x();
|
||||
params[1] = outer.y();
|
||||
params[2] = outer.z();
|
||||
params[3] = outer.w();
|
||||
return;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
params[0] = inner.x();
|
||||
params[1] = inner.y();
|
||||
return;
|
||||
}
|
||||
case GL_VIEWPORT_BOUNDS_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.ViewportBoundsRangeMin;
|
||||
@@ -799,6 +927,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POLYGON_OFFSET_UNITS:
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float-native state, so it is answered here rather than through the integer
|
||||
// fallback: glPolygonOffsetClamp(1, 1, 0.5) must read back as 0.5, not as 0.
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetClamp();
|
||||
return;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.SmoothLineWidthRangeMin;
|
||||
@@ -814,6 +947,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_COVERAGE_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetSampleCoverageValue();
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// Float state, so it has to be answered here rather than through the integer
|
||||
// fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0.
|
||||
params[0] = MG_State::pGLContext->GetMinSampleShadingValue();
|
||||
return;
|
||||
case GL_POINT_FADE_THRESHOLD_SIZE:
|
||||
// Float state: read it directly so the fractional part is not lost to the integer path.
|
||||
params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
|
||||
@@ -1185,6 +1323,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
// The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
|
||||
// not left to the 32-bit fallback below: the conformance suite reads it with
|
||||
// glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
|
||||
// spec requires 2^32-1.
|
||||
params[0] = 0xFFFFFFFFLL;
|
||||
return;
|
||||
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
params[0] = static_cast<GLint64>(
|
||||
@@ -1221,12 +1366,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint ints[4] = {};
|
||||
GetIntegerv(pname, ints);
|
||||
|
||||
// GL 4.6 core 22.1 gives glGetInteger64v the same accepted-pname set as glGetIntegerv, so
|
||||
// every pname the integer getter answers with several components owes them all here too.
|
||||
// A pname that reaches the `default:` arm writes params[0] and leaves the caller's other
|
||||
// components holding whatever they held, with no error to say so.
|
||||
switch (pname) {
|
||||
case GL_BLEND_COLOR:
|
||||
case GL_COLOR_CLEAR_VALUE:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_VIEWPORT:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
params[i] = static_cast<GLint64>(ints[i]);
|
||||
}
|
||||
@@ -1236,6 +1386,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
params[0] = static_cast<GLint64>(ints[0]);
|
||||
params[1] = static_cast<GLint64>(ints[1]);
|
||||
return;
|
||||
@@ -1267,6 +1418,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
count = 2;
|
||||
break;
|
||||
case GL_BLEND_COLOR:
|
||||
@@ -1274,6 +1426,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
count = 4;
|
||||
break;
|
||||
default:
|
||||
@@ -1313,6 +1466,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
// GL_TEXTURE_BUFFER_BINDING and GL_TEXTURE_BUFFER are the same token (0x8C2A): as a
|
||||
// glGetIntegerv pname it asks which BUFFER object is bound to the buffer-texture target,
|
||||
// not which texture is (that one is GL_TEXTURE_BINDING_BUFFER, handled by the texture-unit
|
||||
// decoder above).
|
||||
case GL_TEXTURE_BUFFER_BINDING: {
|
||||
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Texture).GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_BLEND:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
@@ -1368,6 +1530,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// this single case serves every getter flavor.
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
|
||||
return;
|
||||
// glClipControl's two state variables (GL 4.5 core table 23.7). They answer from the
|
||||
// state the entry point records, which is what the conformance suite's initial-value and
|
||||
// set-then-get cases read - the RASTERIZATION half of clip control is a separate,
|
||||
// backend-side question and does not gate the query.
|
||||
case GL_CLIP_ORIGIN:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipOrigin());
|
||||
return;
|
||||
case GL_CLIP_DEPTH_MODE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipDepthMode());
|
||||
return;
|
||||
case GL_COLOR_CLEAR_VALUE: {
|
||||
const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
|
||||
params[0] = static_cast<GLint>(clearColor.x());
|
||||
@@ -1427,19 +1599,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: 0;
|
||||
return;
|
||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||
// KHR_debug floors this at 64. It must agree with what GL_Debug.cpp actually enforces,
|
||||
// or an application that nests to the reported limit would take a STACK_OVERFLOW.
|
||||
*params = kFrontendMaxDebugGroupStackDepth;
|
||||
return;
|
||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||
*params = 1024; // agrees with GL_Debug.cpp's kMaxDebugMessageLength
|
||||
return;
|
||||
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||
// Size of the message log ring; KHR_debug requires at least 1.
|
||||
*params = kFrontendMaxDebugLoggedMessages;
|
||||
return;
|
||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||
*params = 0; // debug-group entrypoints are stubbed
|
||||
// The live depth, which is never 0: GL 4.6 core 20.6 creates the context with one
|
||||
// group already on the stack, and that is the one glPopDebugGroup may not pop.
|
||||
*params = GetDebugGroupStackDepth();
|
||||
return;
|
||||
case GL_CONTEXT_FLAGS: {
|
||||
*params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
|
||||
@@ -1654,6 +1828,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryUniformComponents;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
|
||||
*params = kFrontendMaxGeometryShaderInvocations;
|
||||
return;
|
||||
case GL_MAX_IMAGE_SAMPLES:
|
||||
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
|
||||
return;
|
||||
@@ -1707,6 +1884,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params =
|
||||
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
||||
return;
|
||||
// The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
|
||||
// constant that BuildTBuiltInResource expands, and the two must report the same number
|
||||
// (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
|
||||
// the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
|
||||
// They were the whole per-stage tess family: the table had been filled in only where the
|
||||
// honest answer was zero (the atomic counters, the image uniforms) or where a driver
|
||||
// query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
|
||||
// answer is a real resource count fell through to GL_INVALID_ENUM.
|
||||
case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_PATCH_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
|
||||
return;
|
||||
// Routed through the same clamp as every other per-stage block count so the
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
|
||||
// GL 4.6 table 23.64 cannot be broken by the two families moving independently.
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_SUBROUTINES:
|
||||
*params = kFrontendMaxSubroutines;
|
||||
return;
|
||||
case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
|
||||
*params = kFrontendMaxSubroutineUniformLocations;
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
return;
|
||||
@@ -1752,8 +1982,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_NUM_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_spirv_extensions / GL 4.6 core 22.2. An implementation that advertises no
|
||||
// SPIR-V extension answers zero here, and glGetStringi(GL_SPIR_V_EXTENSIONS, i) is then
|
||||
// never legally called - MobileGL runs the module through its own translation pipeline
|
||||
// and relies on no SPIR-V extension to do it, so zero is the true answer rather than a
|
||||
// placeholder.
|
||||
case GL_NUM_SPIR_V_EXTENSIONS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
|
||||
// agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
|
||||
case GL_NUM_SHADER_BINARY_FORMATS:
|
||||
*params = 0; // ShaderBinary entrypoints are stubbed
|
||||
*params = 1;
|
||||
return;
|
||||
case GL_SHADER_BINARY_FORMATS:
|
||||
*params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
|
||||
return;
|
||||
case GL_PACK_ALIGNMENT:
|
||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
|
||||
@@ -1812,6 +2055,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PRIMITIVE_RESTART_INDEX:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 4.6
|
||||
// core 22.1's float-to-integer rule.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPolygonOffsetClamp()));
|
||||
return;
|
||||
case GL_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0; // program-binary entrypoints are stubbed
|
||||
return;
|
||||
@@ -1897,6 +2145,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_MASK:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_SAMPLE_SHADING:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
|
||||
return;
|
||||
case GL_SAMPLE_MASK_VALUE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||
return;
|
||||
@@ -2115,7 +2370,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
*params = 1024 * 1024; // TODO
|
||||
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||
// state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
|
||||
// uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
|
||||
// indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
|
||||
// that no draw path ever consulted.
|
||||
*params = INT32_MAX;
|
||||
return;
|
||||
case GL_CONTEXT_PROFILE_MASK:
|
||||
// Reports the requested context profile (EGL defaults 3.x contexts to core);
|
||||
@@ -2171,8 +2431,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
break;
|
||||
case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS:
|
||||
// The CLAMPED block count, i.e. exactly what GL_MAX_COMPUTE_UNIFORM_BLOCKS answers.
|
||||
// GL 4.6 table 23.64 defines this as the components reachable through the blocks a
|
||||
// stage may declare, so deriving it from the raw backend number described 256 blocks
|
||||
// an application is only ever allowed 84 of.
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents,
|
||||
dynamicParameters.MaxComputeUniformBlocks,
|
||||
ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
|
||||
@@ -2216,16 +2480,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
||||
break;
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedColorTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||
kFrontendMaxFragmentUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
|
||||
kFrontendMaxGeometryUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
|
||||
@@ -2239,14 +2503,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
|
||||
kFrontendMaxVertexUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||
break;
|
||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedDepthTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||
*params = dynamicParameters.MaxFramebufferWidth;
|
||||
@@ -2273,7 +2537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||
break;
|
||||
case GL_MAX_INTEGER_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedIntegerMaxSamples();
|
||||
break;
|
||||
case GL_MAX_RENDERBUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxRenderbufferSize;
|
||||
@@ -2284,12 +2548,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PATCH_VERTICES:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
|
||||
break;
|
||||
// Float state, so glGetIntegerv rounds it (GL 4.6 core 2.2.2) - the exact values come back
|
||||
// through glGetFloatv. Answered here so glGetBooleanv, which delegates to this getter for
|
||||
// everything its own switch does not handle, does not report INVALID_ENUM for them.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
for (Uint i = 0; i < 4; ++i) params[i] = static_cast<GLint>(std::lround(outer[i]));
|
||||
break;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
|
||||
// GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
|
||||
// the DirectVulkan draw path relies on this staying false (it resolves primitive restart
|
||||
// to "never" for a PATCH_LIST topology on the strength of it).
|
||||
case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
|
||||
*params = GL_FALSE;
|
||||
break;
|
||||
case GL_MAX_PATCH_VERTICES:
|
||||
*params = dynamicParameters.MaxPatchVertices;
|
||||
break;
|
||||
case GL_MAX_TESS_GEN_LEVEL:
|
||||
*params = dynamicParameters.MaxTessGenLevel;
|
||||
break;
|
||||
// Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
|
||||
// COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
|
||||
// conformance suite's own formula exactly, so the two cannot drift.
|
||||
case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
// ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
|
||||
// for a stronger reason: a cull distance discards the whole primitive, so advertising
|
||||
// eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
|
||||
// the honest answer on a host with no cull-distance route, and the conformance suite then
|
||||
// skips the functional cases instead of failing them deep inside a pixel comparison.
|
||||
case GL_MAX_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCullDistances;
|
||||
break;
|
||||
case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCombinedClipAndCullDistances;
|
||||
break;
|
||||
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||
*params = dynamicParameters.MinProgramTextureGatherOffset;
|
||||
break;
|
||||
@@ -2340,7 +2648,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
// ONE, which is under the GL 4.5 core table 23.62 minimum of four and is a known,
|
||||
// deliberate non-conformance. It was briefly raised to 4 on the theory that streams
|
||||
// 1..3 could exist and be permanently empty; measuring that decision refuted it.
|
||||
// Raising the limit un-gates two CTS cases per package across KHR-GL40..GL46 -
|
||||
// transform_feedback.draw_xfb_stream_test (which stops being skipped) and
|
||||
// transform_feedback3.multiple_streams (which stops reporting NotSupported) - and
|
||||
// both then fail, because nothing in the shader pipeline supports layout(stream = N),
|
||||
// EmitStreamVertex or EndStreamPrimitive, and because the query state machine tracks
|
||||
// one active query per TARGET rather than per (target, stream). That is 14 new
|
||||
// failures against 2 gained limits passes, and a 4 nothing can back is the
|
||||
// advertised-caps lie with the sign flipped.
|
||||
//
|
||||
// The real fix is the feature, not the number: per-stream capture needs
|
||||
// layout(stream = N) through the transpiler plus per-(target, stream) query slots,
|
||||
// which DirectVulkan could back with VK_EXT_transform_feedback's geometryStreams and
|
||||
// DirectGLES cannot back at all (ES has no vertex streams). Until that lands, one is
|
||||
// the honest count and every stream-addressing entry point bounds itself by THIS
|
||||
// query, so raising it later moves them all together.
|
||||
*params = kFrontendMaxVertexStreams;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
@@ -2357,15 +2683,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxTextureSize;
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
|
||||
// Never advertise more bindings than the state layer's indexed-binding array can track
|
||||
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
|
||||
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
|
||||
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
|
||||
// (36), so the clamp never under-advertises.
|
||||
// advertised index and expects no error. The floor is the GL 4.5 core minimum, and
|
||||
// the array was widened to exactly it, so the two coincide by construction.
|
||||
//
|
||||
// WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
|
||||
// family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
|
||||
// not a driver binding point. Neither backend forwards it as one on the draw path:
|
||||
// DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
|
||||
// (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
|
||||
// resolves each block to a descriptor. So what the host driver's count bounds is how
|
||||
// many blocks ONE PROGRAM may use, not how many points an application may bind.
|
||||
//
|
||||
// That per-program number is NOT GL_MAX_COMBINED_UNIFORM_BLOCKS (84, the six-stage
|
||||
// sum): no single program can reach it. A graphics program is bounded by the five
|
||||
// graphics stages' per-stage counts, 14 each, so 70 blocks plus the global UBO at ES
|
||||
// point 0 = 71 - inside the ES 3.2 minimum of 72. A compute program is bounded by
|
||||
// GL_MAX_COMPUTE_UNIFORM_BLOCKS, which on DirectGLES is the ES driver's own count
|
||||
// (GL-scale, ~14) and on DirectVulkan is served from descriptors with no ES binding
|
||||
// points involved. Raising any per-stage graphics count past 14 is what would break
|
||||
// this, so that is the edit to check against the ES ceiling - not this one.
|
||||
static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
|
||||
kFrontendMinUniformBufferBindings,
|
||||
"the indexed-binding array must be able to hold every advertised uniform binding point");
|
||||
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
|
||||
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
|
||||
break;
|
||||
}
|
||||
case GL_MAX_UNIFORM_BLOCK_SIZE:
|
||||
*params = dynamicParameters.MaxUniformBlockSize;
|
||||
break;
|
||||
@@ -2403,7 +2750,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
|
||||
break;
|
||||
case GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<GLint>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
// The STORAGE alignment, which is its own limit - this used to answer with the
|
||||
// uniform one. They differ on real hardware (Adreno 830: 32 uniform, 64 storage), and
|
||||
// under-reporting it is silent: ValidateBindBufferRange accepts the offset, the ES
|
||||
// driver accepts it too without raising an error, and the shader's writes then land
|
||||
// at an address the application never bound.
|
||||
*params = static_cast<GLint>(dynamicParameters.ShaderStorageBufferOffsetAlignment);
|
||||
break;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
|
||||
|
||||
@@ -25,7 +25,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
|
||||
// core minimum. Frontend multisample validators have to honour this ceiling for every
|
||||
// format, otherwise MobileGL rejects a sample count it advertised itself.
|
||||
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
|
||||
// ceilings below have a minimum of one and are reported as probed.
|
||||
GLint GetAdvertisedMaxSamples();
|
||||
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
|
||||
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
|
||||
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
|
||||
// the query promised - it used to floor both at 4 and then let the backend quietly
|
||||
// under-allocate whatever the driver could not actually provide.
|
||||
GLint GetAdvertisedColorTextureMaxSamples();
|
||||
GLint GetAdvertisedDepthTextureMaxSamples();
|
||||
GLint GetAdvertisedIntegerMaxSamples();
|
||||
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
|
||||
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
|
||||
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
|
||||
//
|
||||
// Shared rather than duplicated because two callers need the identical number and disagreeing
|
||||
// would be a silent bug: the query itself, and the draw path's write of the reserved
|
||||
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
|
||||
// is exactly what the sample_variables CTS does.
|
||||
GLint ResolveDrawFramebufferSampleCount();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "Config.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -30,10 +32,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckShaderNameValidity(Uint shader) {
|
||||
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) {
|
||||
// The mirror of CheckProgramNameValidity below, and for the same reason: programs and
|
||||
// shaders are drawn from ONE name space (ProgramState hands both out of a single
|
||||
// generator), so a name that exists but belongs to a PROGRAM is the wrong kind of
|
||||
// object - GL 3.3 core 2.11.x makes that INVALID_OPERATION - while a name GL never
|
||||
// handed out is INVALID_VALUE. This half of the split was missing, so every shader
|
||||
// entry point handed a program name reported INVALID_VALUE; the conformance suite
|
||||
// reads exactly that code back from glSpecializeShader.
|
||||
const ErrorCode error = (shader != 0 && MG_State::pGLContext->ValidateProgramName(shader))
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shader) + " is not a valid name."));
|
||||
std::to_string(shader) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a shader object."
|
||||
: " is not a valid name.")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -245,6 +259,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6.3: INVALID_VALUE when uniformBlockBinding >= MAX_UNIFORM_BUFFER_BINDINGS.
|
||||
// The storage-block twin below has always had this check; the uniform one never did, and the
|
||||
// value it stores is used as a RAW SUBSCRIPT into the state layer's fixed indexed-binding
|
||||
// array on every draw and dispatch (DirectGLES's per-program UBO rebind, DirectVulkan's
|
||||
// descriptor resolve, whose only guard is a MOBILEGL_ASSERT that compiles away in release).
|
||||
// An out-of-range binding therefore did not merely go unreported - it read past the array and
|
||||
// dereferenced whatever SharedPtr it found there.
|
||||
bool ValidateUniformBlockBinding(GLuint binding) {
|
||||
// Exactly what glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS) advertises: the state
|
||||
// layer's array width, which the getter clamps to as well.
|
||||
const SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform);
|
||||
if (binding < maxBindingCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("Uniform block binding {} is not less than GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).", binding,
|
||||
maxBindingCount)));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateShaderStorageBlockBinding(GLuint binding) {
|
||||
SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
@@ -307,9 +345,195 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompileShader_State(GLuint shader) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
// ARB_gl_spirv: "INVALID_OPERATION is generated by CompileShader if shader has been
|
||||
// associated with a SPIR-V binary". Such an object has no GLSL source to compile - it is
|
||||
// waiting for glSpecializeShader, which is the operation that compiles it.
|
||||
if (shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" holds a SPIR-V binary; use glSpecializeShader instead of glCompileShader."));
|
||||
return;
|
||||
}
|
||||
shaderObject->Compile();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// GL_ARB_gl_spirv
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
void ShaderBinary_State(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary,
|
||||
GLsizei length) {
|
||||
if (count < 0 || length < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count and length must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL_NUM_SHADER_BINARY_FORMATS advertises exactly one format, so every other value is
|
||||
// INVALID_ENUM (GL 4.6 core 7.2). This is the check that used to be missing entirely -
|
||||
// the entry point was a silent stub, so an application handed a format nothing supports
|
||||
// and was told nothing.
|
||||
if (binaryformat != GL_SHADER_BINARY_FORMAT_SPIR_V) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"binaryformat must be GL_SHADER_BINARY_FORMAT_SPIR_V."));
|
||||
return;
|
||||
}
|
||||
if (count == 0) return;
|
||||
if (shaders == nullptr || (length > 0 && binary == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "shaders and binary must not be null."));
|
||||
return;
|
||||
}
|
||||
// A SPIR-V module is a sequence of 32-bit words, so a length that is not a multiple of
|
||||
// four cannot be one (ARB_gl_spirv makes this INVALID_VALUE).
|
||||
if ((length % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"length must be a multiple of four for a SPIR-V module."));
|
||||
return;
|
||||
}
|
||||
|
||||
// EVERY name is validated before ANY of them is written: the entry point is all-or-
|
||||
// nothing, and half-applying it would leave some objects holding a module the call was
|
||||
// rejected for. The duplicate check is the extension's own ("INVALID_VALUE ... if the
|
||||
// same shader object is specified more than once").
|
||||
std::set<GLuint> seen;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!seen.insert(shaders[i]).second) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shaders[i]) +
|
||||
" appears more than once in `shaders`."));
|
||||
return;
|
||||
}
|
||||
if (!MG_State::pGLContext->ValidateShaderName(shaders[i])) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shaders[i]) + " is not the name of a shader object."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT wordCount = static_cast<SizeT>(length) / 4;
|
||||
Vector<Uint32> module(wordCount);
|
||||
if (wordCount != 0) {
|
||||
Memcpy(module.data(), binary, static_cast<SizeT>(length));
|
||||
}
|
||||
// spirv-val here, not at glSpecializeShader: this is where the words arrive, and past it
|
||||
// they reach SPIRV-Cross, which parses rather than validates. ARB_gl_spirv lets an
|
||||
// implementation reject an invalid module at either call; rejecting at the earlier one
|
||||
// means the application's error is reported next to the data that caused it.
|
||||
if (const auto validated = MG_Util::ShaderTranspiler::ShaderCompiler::ValidateSpirvModule(module);
|
||||
!validated) {
|
||||
MGLOG_D("%s: rejected SPIR-V module: %s", __func__, validated.error().log.c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, validated.error().log));
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
auto& shaderObject = TryToGetShaderObject(shaders[i]);
|
||||
if (!shaderObject) continue;
|
||||
// A copy per object, not a shared buffer: each shader object may be specialized with
|
||||
// different constants, and each specialization re-reads its own original words.
|
||||
Vector<Uint32> perObject = module;
|
||||
shaderObject->SetSpirvBinary(Move(perObject));
|
||||
}
|
||||
}
|
||||
|
||||
void SpecializeShader_State(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
if (!shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has no SPIR-V binary; call glShaderBinary first."));
|
||||
return;
|
||||
}
|
||||
// ARB_gl_spirv: a shader that has already been specialized may not be specialized again
|
||||
// until glShaderBinary re-associates a module with it.
|
||||
if (shaderObject->HasBeenSpecialized()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has already been specialized; re-associate its module with "
|
||||
"glShaderBinary before specializing it again."));
|
||||
return;
|
||||
}
|
||||
// pEntryPoint names the entry point to specialize; there is no default. A null pointer
|
||||
// cannot name one, and neither can the empty string.
|
||||
if (pEntryPoint == nullptr || *pEntryPoint == '\0') {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pEntryPoint must name an entry point."));
|
||||
return;
|
||||
}
|
||||
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pConstantIndex and pConstantValue must not be null."));
|
||||
return;
|
||||
}
|
||||
// "INVALID_VALUE is generated if any value in pConstantIndex is repeated" - checked before
|
||||
// anything is applied, for the same all-or-nothing reason glShaderBinary checks its names
|
||||
// up front.
|
||||
Vector<Uint32> constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants);
|
||||
Vector<Uint32> constantValues(pConstantValue, pConstantValue + numSpecializationConstants);
|
||||
{
|
||||
std::set<Uint32> seen;
|
||||
for (const Uint32 id : constantIds) {
|
||||
if (seen.insert(id).second) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"constant index " + std::to_string(id) + " is repeated."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const String entryPoint(pEntryPoint);
|
||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
|
||||
using SpecializationFailure = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializationFailure;
|
||||
SpecializationFailure failure = SpecializationFailure::None;
|
||||
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
|
||||
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
|
||||
if (!specialized) {
|
||||
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
|
||||
specialized.error().log.c_str());
|
||||
// The two conditions ARB_gl_spirv ENUMERATES are GL errors, and an erroring GL command
|
||||
// must have no other effect - so the shader object is left exactly as it was rather
|
||||
// than being pushed into a failed-compile state. Anything else is a genuine compile
|
||||
// failure of a well-formed request, which the extension routes through COMPILE_STATUS
|
||||
// and the info log exactly as glCompileShader does.
|
||||
if (failure == SpecializationFailure::UnknownEntryPoint ||
|
||||
failure == SpecializationFailure::UnknownConstantId) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
|
||||
specialized.value().xfbBufferMode);
|
||||
}
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
|
||||
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
|
||||
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
|
||||
@@ -744,12 +968,77 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_PROGRAM_SEPARABLE:
|
||||
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
|
||||
// The LATCHED flag, not the live one: glProgramParameteri's write takes effect at the
|
||||
// next link (GL 4.6 core 7.3), so a program told to be separable and then never
|
||||
// linked still reports GL_FALSE.
|
||||
*params = programObject->GetLinkedSeparable() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
|
||||
// The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as
|
||||
// GL_COMPUTE_WORK_GROUP_SIZE above, and for the same reason: "a linked program object
|
||||
// with a geometry shader" is one whose EXECUTABLE has the stage, so an
|
||||
// attached-but-not-yet-linked shader must give INVALID_OPERATION rather than the previous
|
||||
// link's value. The geometry three used to be listed here only to fall through into the
|
||||
// INVALID_ENUM default, and the tessellation five were not listed at all.
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a geometry shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_GEOMETRY_VERTICES_OUT: *params = programObject->GetGeometryVerticesOut(); break;
|
||||
case GL_GEOMETRY_INPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryInputType()); break;
|
||||
case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryOutputType()); break;
|
||||
default: *params = programObject->GetGeometryShaderInvocations(); break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_CONTROL_OUTPUT_VERTICES: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessControl)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation control shader."));
|
||||
return;
|
||||
}
|
||||
*params = programObject->GetTessControlOutputVertices();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_GEN_MODE:
|
||||
case GL_TESS_GEN_SPACING:
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
case GL_TESS_GEN_POINT_MODE: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessEval)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation evaluation shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_TESS_GEN_MODE: *params = static_cast<GLint>(programObject->GetTessGenMode()); break;
|
||||
case GL_TESS_GEN_SPACING: *params = static_cast<GLint>(programObject->GetTessGenSpacing()); break;
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
*params = static_cast<GLint>(programObject->GetTessGenVertexOrder());
|
||||
break;
|
||||
default: *params = programObject->GetTessGenPointMode() ? GL_TRUE : GL_FALSE; break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -811,8 +1100,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||
break;
|
||||
case GL_SHADER_SOURCE_LENGTH:
|
||||
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
|
||||
case GL_SHADER_SOURCE_LENGTH: {
|
||||
// The APPLICATION's source, which is empty for a shader that came from glShaderBinary -
|
||||
// see ShaderObject::GetApplicationShaderSource.
|
||||
const auto& source = shaderObject->GetApplicationShaderSource();
|
||||
*params = source.empty() ? 0 : (GLint)source.length() + 1;
|
||||
break;
|
||||
}
|
||||
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
|
||||
// while the object stands for an application-supplied module. It is the FIRST thing the
|
||||
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
|
||||
// default arm below and take the whole test with it.
|
||||
case GL_SPIR_V_BINARY:
|
||||
*params = shaderObject->HasSpirvBinary() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
|
||||
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
|
||||
@@ -858,13 +1158,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
|
||||
auto& src = shaderObject->GetShaderSource();
|
||||
auto& src = shaderObject->GetApplicationShaderSource();
|
||||
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
|
||||
}
|
||||
|
||||
GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return -1;
|
||||
// GL 4.6 core 7.6: "INVALID_OPERATION is generated if program has not been successfully
|
||||
// linked". Answering -1 silently is not the same thing - the conformance suite reads the
|
||||
// error, not the location.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return -1;
|
||||
}
|
||||
auto loc = programObject->GetUniformLocation(name);
|
||||
MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
|
||||
return loc;
|
||||
@@ -1277,11 +1587,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
template <GLsizei ItemCount, typename T>
|
||||
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
// The link check comes BEFORE the location == -1 early-out, not after. GL 4.6 core 7.6
|
||||
// makes an unlinked program INVALID_OPERATION regardless of the location, and -1 is
|
||||
// exactly the location an application holds after glGetUniformLocation on such a program -
|
||||
// so checking -1 first swallowed the very case the rule exists for.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1289,6 +1601,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
// "If location is equal to -1, the data passed in will be silently ignored and the
|
||||
// specified uniform variable will not be changed" - after the program itself has been
|
||||
// found acceptable.
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
@@ -1699,8 +2015,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1712,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1731,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1756,8 +2072,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1769,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1790,8 +2106,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
|
||||
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1803,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
@@ -1836,6 +2152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Program object" + std::to_string(program) + " that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateUniformBlockBinding(uniformBlockBinding)) return;
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -2083,6 +2400,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BindAttribLocation_State(program, index, name);
|
||||
}
|
||||
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) {
|
||||
ShaderBinary_State(count, shaders, binaryformat, binary, length);
|
||||
}
|
||||
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
SpecializeShader_State(shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue);
|
||||
}
|
||||
|
||||
void CompileShader(GLuint shader) {
|
||||
CompileShader_State(shader);
|
||||
}
|
||||
@@ -2342,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2352,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
|
||||
}
|
||||
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2368,7 +2694,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2378,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
|
||||
}
|
||||
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2394,7 +2720,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2404,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
|
||||
}
|
||||
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2420,7 +2746,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2430,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
|
||||
}
|
||||
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2446,7 +2772,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2456,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
|
||||
}
|
||||
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2472,7 +2798,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2482,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
|
||||
}
|
||||
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2498,7 +2824,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2508,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
|
||||
}
|
||||
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2524,7 +2850,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2534,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
|
||||
}
|
||||
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2550,7 +2876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2560,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
|
||||
}
|
||||
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void AttachShader(GLuint program, GLuint shader);
|
||||
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
|
||||
void CompileShader(GLuint shader);
|
||||
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
|
||||
// the module to one or more shader objects, glSpecializeShader names its entry point and
|
||||
// supplies its specialization constants and is what actually compiles them.
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue);
|
||||
GLuint CreateProgram(void);
|
||||
GLuint CreateShader(GLenum type);
|
||||
void DeleteProgram(GLuint program);
|
||||
|
||||
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Program {} has not been linked successfully.", program));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
|
||||
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
|
||||
// whose live flag was cleared after a separable link is still a legal stage, and a
|
||||
// program whose live flag was set after a non-separable link is not.
|
||||
if (!programObject->GetLinkedSeparable()) {
|
||||
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||
std::format("Program {} was not linked as a separable program.", program));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
|
||||
|
||||
@@ -59,6 +59,62 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
|
||||
// target (0 = none). A map rather than a field per target: the eleven behave identically
|
||||
// and none of them has any state beyond "which object is counting".
|
||||
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
|
||||
|
||||
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
|
||||
// ADVERTISED list rather than from a capability bit for the same reason
|
||||
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
|
||||
// support" and "the tessellation-conditional API surface is open" the same fact by
|
||||
// construction, so the day a backend starts advertising the string the surface below opens
|
||||
// with it and no second edit is owed.
|
||||
Bool AdvertisesTessellationShaderExtension() {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) return false;
|
||||
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
|
||||
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
|
||||
}
|
||||
|
||||
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
|
||||
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
|
||||
// by which an application could learn otherwise before calling. MobileGL instruments none of
|
||||
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
|
||||
// zero for these targets, which is the spec's own signal that the counter is unsupported and
|
||||
// its results indeterminate. That is an honest zero, not an advertised capability - the
|
||||
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
|
||||
// informative.
|
||||
//
|
||||
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
|
||||
// makes them conditional on tessellation support rather than unconditional, and the only
|
||||
// thing an application (or the conformance suite) can read to decide whether an
|
||||
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
|
||||
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
|
||||
// tessellation-conditional token while withholding the string that announces the condition
|
||||
// is self-contradictory, and it is the contradiction the suite catches
|
||||
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
|
||||
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
|
||||
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
|
||||
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
|
||||
switch (target) {
|
||||
case GL_VERTICES_SUBMITTED:
|
||||
case GL_PRIMITIVES_SUBMITTED:
|
||||
case GL_VERTEX_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
|
||||
case GL_FRAGMENT_SHADER_INVOCATIONS:
|
||||
case GL_COMPUTE_SHADER_INVOCATIONS:
|
||||
case GL_CLIPPING_INPUT_PRIMITIVES:
|
||||
case GL_CLIPPING_OUTPUT_PRIMITIVES:
|
||||
return true;
|
||||
case GL_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
return AdvertisesTessellationShaderExtension();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -370,6 +426,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
|
||||
queryObject->active = false;
|
||||
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
@@ -410,7 +469,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
@@ -426,10 +487,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on this target.");
|
||||
@@ -448,7 +511,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
|
||||
// The object still becomes a real, target-latched query so every other rule about it
|
||||
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
|
||||
} else if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
@@ -476,15 +543,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
return;
|
||||
@@ -494,6 +565,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// The result is a definite zero rather than an unread backend handle, so a later
|
||||
// GetQueryObject* answers immediately and never waits on something that was never
|
||||
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
|
||||
queryObject->cachedResult = 0;
|
||||
queryObject->resultCached = true;
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
@@ -657,7 +739,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
const auto it = g_activePipelineStatisticsQueryIds.find(target);
|
||||
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
|
||||
} else {
|
||||
*params = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -668,6 +755,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins.
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
|
||||
// results are indeterminate. The conformance suite reads exactly this and skips
|
||||
// the functional half of each such target, which is the outcome an uninstrumented
|
||||
// counter should produce.
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
@@ -741,14 +836,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsPerVertexStreamQueryTarget(GLenum target) {
|
||||
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
}
|
||||
|
||||
// The indexed query entry points differ from the plain ones only in the vertex
|
||||
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
|
||||
// for the two transform feedback targets and zero for every other target. With a
|
||||
// single vertex stream both bounds are 1, so a valid call is always index 0 and
|
||||
// forwards to the unindexed implementation.
|
||||
// for the two transform feedback targets and zero for every other target. MobileGL
|
||||
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
|
||||
// which is what makes the three forwards below equivalent to the unindexed entry points.
|
||||
//
|
||||
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
|
||||
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
|
||||
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
|
||||
// per-TARGET globals and would end - or report - a query begun on a different stream.
|
||||
// Raising that limit therefore means giving each active query a stream index and
|
||||
// comparing it here, not just changing the number.
|
||||
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
|
||||
const Bool perStreamTarget =
|
||||
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
|
||||
GLint maxVertexStreams = 1;
|
||||
if (perStreamTarget) {
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
@@ -761,6 +866,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: "index must be zero for this query target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
|
||||
|
||||
@@ -328,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
|
||||
}
|
||||
|
||||
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
|
||||
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
|
||||
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
|
||||
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
|
||||
void MinSampleShading_State(GLfloat value) {
|
||||
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void PolygonOffset_State(GLfloat factor, GLfloat units) {
|
||||
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
|
||||
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
|
||||
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
|
||||
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
|
||||
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
|
||||
static_cast<Float>(clamp));
|
||||
}
|
||||
|
||||
void ClipControl_State(GLenum origin, GLenum depth) {
|
||||
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
|
||||
// GL_INVALID_ENUM with the state left untouched.
|
||||
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
|
||||
MG_Util::ConvertGLEnumToString(origin) + "."));
|
||||
return;
|
||||
}
|
||||
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
|
||||
MG_Util::ConvertGLEnumToString(depth) + "."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->SetClipControl(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode_State(GLenum face, GLenum mode) {
|
||||
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
|
||||
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
|
||||
@@ -1013,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SampleCoverage_State(value, invert);
|
||||
}
|
||||
|
||||
void MinSampleShading(GLfloat value) {
|
||||
MinSampleShading_State(value);
|
||||
}
|
||||
|
||||
void PolygonOffset(GLfloat factor, GLfloat units) {
|
||||
PolygonOffset_State(factor, units);
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
PolygonOffsetClamp_State(factor, units, clamp);
|
||||
}
|
||||
|
||||
void ClipControl(GLenum origin, GLenum depth) {
|
||||
ClipControl_State(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode(GLenum face, GLenum mode) {
|
||||
PolygonMode_State(face, mode);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void StencilFunc(GLenum func, GLint ref, GLuint mask);
|
||||
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
void SampleCoverage(GLfloat value, GLboolean invert);
|
||||
void MinSampleShading(GLfloat value);
|
||||
void PolygonOffset(GLfloat factor, GLfloat units);
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
|
||||
void ClipControl(GLenum origin, GLenum depth);
|
||||
void PolygonMode(GLenum face, GLenum mode);
|
||||
void PointSize(GLfloat size);
|
||||
void PointParameterf(GLenum pname, GLfloat param);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return static_cast<Float>(*(const GLint*)param);
|
||||
}
|
||||
|
||||
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
|
||||
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
|
||||
// reads exactly one component and does not care.
|
||||
Bool IsVectorOnlySamplerPname(GLenum pname) {
|
||||
return pname == GL_TEXTURE_BORDER_COLOR;
|
||||
}
|
||||
|
||||
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
|
||||
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
|
||||
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
|
||||
// sampler path funnels all three spellings through one void* function, which is precisely how
|
||||
// it came to write a fixed type regardless of the caller.
|
||||
//
|
||||
// Truncation rather than rounding for the float -> integer direction, matching the texture
|
||||
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
|
||||
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
|
||||
// sampler queried the same way must answer the same number.
|
||||
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = value;
|
||||
return;
|
||||
}
|
||||
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
|
||||
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
|
||||
// conversion is the well-defined modular one, and it is what the texture-side
|
||||
// GetTexParameterIuiv fallback does.
|
||||
const GLint asInt = static_cast<GLint>(value);
|
||||
if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(asInt);
|
||||
} else {
|
||||
*(GLint*)params = asInt;
|
||||
}
|
||||
}
|
||||
|
||||
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = static_cast<GLfloat>(value);
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = value;
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(value);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
|
||||
if (param == nullptr) return false;
|
||||
|
||||
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
|
||||
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
|
||||
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
|
||||
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
|
||||
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
|
||||
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
|
||||
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
|
||||
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (param == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isFloat) {
|
||||
const auto* values = (const GLfloat*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else if (!isIntegerCommand) {
|
||||
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
|
||||
const auto* values = (const GLint*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto* values = (const GLuint*)param;
|
||||
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
|
||||
@@ -128,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (params == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
|
||||
|
||||
using namespace MG_Util;
|
||||
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
|
||||
// decides the destination type. Writing a fixed type regardless - which is what these case
|
||||
// labels used to do - hands back the other type's bit pattern rather than a converted value:
|
||||
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
|
||||
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
|
||||
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
|
||||
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
|
||||
// how the same bug class was already found and fixed once for a single pname.
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_WRAP_S:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_T:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_R:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
|
||||
samplerObj->GetMipmapMode()));
|
||||
break;
|
||||
case GL_TEXTURE_MAG_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
|
||||
SamplerMipmapMode::None));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMinLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMaxLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
|
||||
break;
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
*(GLfloat*)params = samplerObj->GetLodBias();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
|
||||
}
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_FUNC:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
if (isFloat) {
|
||||
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
out[1] = color.y();
|
||||
out[2] = color.z();
|
||||
out[3] = color.w();
|
||||
} else if (!isIntegerCommand) {
|
||||
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
|
||||
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
|
||||
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
|
||||
const auto& color = samplerObj->GetBorderColor();
|
||||
auto* out = (GLint*)params;
|
||||
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
|
||||
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
|
||||
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
|
||||
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto& color = samplerObj->GetBorderColorUI();
|
||||
auto* out = (GLuint*)params;
|
||||
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sampler == 0) {
|
||||
textureUnit.SetSamplerObject(nullptr);
|
||||
} else {
|
||||
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
|
||||
// name, which is why this cannot go through the shared SamplerImpl validator.
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
|
||||
// name, so the shared validator answers for all of them.
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
|
||||
if (!doesSamplerObjectCreated) {
|
||||
MG_State::pGLContext->CreateSamplerObject(sampler);
|
||||
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, true);
|
||||
SetSamplerParam_State(sampler, pname, param, false, true, true);
|
||||
}
|
||||
|
||||
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, true);
|
||||
}
|
||||
|
||||
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, true, false);
|
||||
SetSamplerParam_State(sampler, pname, param, true, false, false);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
|
||||
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
|
||||
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
|
||||
// past the caller's single stack scalar - taking the address of a by-value argument and handing
|
||||
// it to a four-component reader is what these used to do. The texture-side twins already answer
|
||||
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
|
||||
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameteriv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameterfv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, true);
|
||||
GetSamplerParam_State(sampler, pname, params, false, true, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, true, false);
|
||||
GetSamplerParam_State(sampler, pname, params, true, false, false);
|
||||
}
|
||||
|
||||
void GenSamplers(GLsizei count, GLuint* samplers) {
|
||||
|
||||
@@ -12,11 +12,17 @@
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
|
||||
// sampler object previously returned from a call to GenSamplers." That class is shared by every
|
||||
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
|
||||
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
|
||||
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
|
||||
Bool ValidateSamplerName(GLuint sampler) {
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -69,8 +69,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
|
||||
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
|
||||
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
|
||||
// flush it never got.
|
||||
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
|
||||
// the enum alone left glGetError() clean and the failure indistinguishable from a
|
||||
// genuine wait failure on a live sync.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
||||
@@ -95,6 +112,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
return;
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
@@ -125,8 +145,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
||||
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
|
||||
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
|
||||
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
|
||||
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
|
||||
// but a mistyped pname answered a bare 0 that no query ever returns.
|
||||
if (bufSize < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
|
||||
return;
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
@@ -152,7 +186,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
value = static_cast<GLint>(syncObject->flags);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
|
||||
"GL_SYNC_FLAGS."));
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (length) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,24 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
|
||||
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
|
||||
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
|
||||
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
|
||||
//
|
||||
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
|
||||
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
|
||||
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
|
||||
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
|
||||
// that adds row padding / a row-length override / a skip offset, is rejected with
|
||||
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
|
||||
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
|
||||
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
|
||||
// clears take the same list, so it is shared rather than written out twice.
|
||||
Bool IsBufferTextureInternalFormat(GLenum internalformat);
|
||||
@@ -37,8 +52,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum format, GLenum type, const void* pixels);
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
||||
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
GLsizei imageSize, const void* data);
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
|
||||
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
||||
const void* data);
|
||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||
@@ -60,6 +80,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
|
||||
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
|
||||
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
|
||||
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
|
||||
GLuint numlevels, GLuint minlayer, GLuint numlayers);
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
|
||||
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller) {
|
||||
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
|
||||
return true;
|
||||
}
|
||||
if (width != height) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array levels must be square (width == height)"));
|
||||
return false;
|
||||
}
|
||||
if (depth % 6 != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array depth must be a multiple of six"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
|
||||
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
|
||||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
|
||||
@@ -623,4 +645,144 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
|
||||
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
|
||||
// enum collapses every compressed format onto uncompressed storage and drops formats it
|
||||
// cannot carry, so classifying the converted value would silently widen the compatibility
|
||||
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case GL_RGBA32F:
|
||||
case GL_RGBA32UI:
|
||||
case GL_RGBA32I:
|
||||
return TextureViewClass::Bits128;
|
||||
case GL_RGB32F:
|
||||
case GL_RGB32UI:
|
||||
case GL_RGB32I:
|
||||
return TextureViewClass::Bits96;
|
||||
case GL_RGBA16F:
|
||||
case GL_RG32F:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RG32UI:
|
||||
case GL_RGBA16I:
|
||||
case GL_RG32I:
|
||||
case GL_RGBA16:
|
||||
case GL_RGBA16_SNORM:
|
||||
return TextureViewClass::Bits64;
|
||||
case GL_RGB16:
|
||||
case GL_RGB16_SNORM:
|
||||
case GL_RGB16F:
|
||||
case GL_RGB16UI:
|
||||
case GL_RGB16I:
|
||||
return TextureViewClass::Bits48;
|
||||
case GL_RG16F:
|
||||
case GL_R11F_G11F_B10F:
|
||||
case GL_R32F:
|
||||
case GL_RGB10_A2UI:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RG16UI:
|
||||
case GL_R32UI:
|
||||
case GL_RGBA8I:
|
||||
case GL_RG16I:
|
||||
case GL_R32I:
|
||||
case GL_RGB10_A2:
|
||||
case GL_RGBA8:
|
||||
case GL_RG16:
|
||||
case GL_RGBA8_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
case GL_SRGB8_ALPHA8:
|
||||
case GL_RGB9_E5:
|
||||
return TextureViewClass::Bits32;
|
||||
case GL_RGB8:
|
||||
case GL_RGB8_SNORM:
|
||||
case GL_SRGB8:
|
||||
case GL_RGB8UI:
|
||||
case GL_RGB8I:
|
||||
return TextureViewClass::Bits24;
|
||||
case GL_R16F:
|
||||
case GL_RG8UI:
|
||||
case GL_R16UI:
|
||||
case GL_RG8I:
|
||||
case GL_R16I:
|
||||
case GL_RG8:
|
||||
case GL_R16:
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R16_SNORM:
|
||||
return TextureViewClass::Bits16;
|
||||
case GL_R8UI:
|
||||
case GL_R8I:
|
||||
case GL_R8:
|
||||
case GL_R8_SNORM:
|
||||
return TextureViewClass::Bits8;
|
||||
case GL_COMPRESSED_RED_RGTC1:
|
||||
case GL_COMPRESSED_SIGNED_RED_RGTC1:
|
||||
return TextureViewClass::Rgtc1Red;
|
||||
case GL_COMPRESSED_RG_RGTC2:
|
||||
case GL_COMPRESSED_SIGNED_RG_RGTC2:
|
||||
return TextureViewClass::Rgtc2Rg;
|
||||
case GL_COMPRESSED_RGBA_BPTC_UNORM:
|
||||
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
|
||||
return TextureViewClass::BptcUnorm;
|
||||
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
|
||||
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
|
||||
return TextureViewClass::BptcFloat;
|
||||
default:
|
||||
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
|
||||
// reaches here. The caller must then demand an EXACT format match.
|
||||
return TextureViewClass::None;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
|
||||
switch (origTarget) {
|
||||
case TextureTarget::Texture1D:
|
||||
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
|
||||
case TextureTarget::Texture2D:
|
||||
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
|
||||
case TextureTarget::Texture3D:
|
||||
return viewTarget == TextureTarget::Texture3D;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureRectangle:
|
||||
return viewTarget == TextureTarget::TextureRectangle;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
|
||||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return viewTarget == TextureTarget::Texture2DMultisample ||
|
||||
viewTarget == TextureTarget::Texture2DMultisampleArray;
|
||||
case TextureTarget::TextureBuffer:
|
||||
// The table lists no legal target for a buffer texture: its storage is a buffer
|
||||
// object, and there is nothing to make a view of.
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
|
||||
switch (viewTarget) {
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return 6;
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return 1;
|
||||
default:
|
||||
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
|
||||
// "multiple of 6" is checked by the caller).
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
|
||||
Bool ValidateTextureLevelNumber(Int level);
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
|
||||
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
|
||||
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
|
||||
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
|
||||
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
|
||||
// non-cube-array upload target answers true untouched.
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller);
|
||||
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
|
||||
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
|
||||
Bool ValidateTextureBorderNumber(Int border);
|
||||
@@ -79,4 +86,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
// 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);
|
||||
|
||||
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
|
||||
// table, which the spec turns into a much stricter rule than "same class": such a format can
|
||||
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
|
||||
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
|
||||
enum class TextureViewClass {
|
||||
None = 0,
|
||||
Bits128,
|
||||
Bits96,
|
||||
Bits64,
|
||||
Bits48,
|
||||
Bits32,
|
||||
Bits24,
|
||||
Bits16,
|
||||
Bits8,
|
||||
Rgtc1Red,
|
||||
Rgtc2Rg,
|
||||
BptcUnorm,
|
||||
BptcFloat,
|
||||
};
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat);
|
||||
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
|
||||
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
|
||||
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
|
||||
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
|
||||
// caller because its constraint is "a multiple of 6", not an exact count.
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
Uint GetMaxVertexAttribs() {
|
||||
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!MG_Backend::pActiveBackendObject) return capacity;
|
||||
|
||||
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
|
||||
if (backendLimit <= 0) return capacity;
|
||||
return std::min(static_cast<Uint>(backendLimit), capacity);
|
||||
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
|
||||
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
|
||||
const Int backendLimit =
|
||||
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
|
||||
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribBindings() {
|
||||
|
||||
@@ -58,13 +58,18 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DrawParametersScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
Scenarios/SampleMaskScopeScenario.cpp
|
||||
Scenarios/SampledSetStalenessScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/SnormAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
Scenarios/AdvertisedLimitsScenario.cpp
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/PrimitiveRestartScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/SampleVariablesScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
@@ -86,6 +91,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||
Scenarios/UnlocatedIoBlockScenario.cpp
|
||||
Scenarios/TessellationDrawModeScenario.cpp
|
||||
Scenarios/GeometryDrawModeScenario.cpp
|
||||
Scenarios/PostLinkAttachScenario.cpp
|
||||
@@ -95,16 +101,31 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||
Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||
Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
Scenarios/PointSizeDemotionScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/CopyImagePacked16Scenario.cpp
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredAttachmentShapeScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/LargeArenaAdoptionScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/SpirvShaderBinaryScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
Scenarios/GuiBatchScenario.cpp
|
||||
Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -270,9 +291,9 @@ if (MOBILEGL_ITEST_VK_ICD)
|
||||
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
||||
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
||||
list(APPEND MGL_ITEST_VULKAN_ENV
|
||||
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
|
||||
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
|
||||
"MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||
"MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1"
|
||||
"MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -303,6 +324,70 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# The shader-compiler configurations AsyncCompileScenario needs, and the one
|
||||
# ViewportArrayScenario's negative control needs.
|
||||
#
|
||||
# These used to be poked into MG_Config::Features from inside the test bodies. They
|
||||
# cannot be any more - on Android this module links the SHIPPING libMobileGL.so, which
|
||||
# exports nothing internal - and they should not have been anyway: half of what each of
|
||||
# them decides is latched before the first GL call (the compile pool and its threads;
|
||||
# the advertised extension list, which a backend builds once from the configuration in
|
||||
# force at its first use), so an in-process write could only ever have moved the other
|
||||
# half. Every one of them is a whole-process property, and a whole-process property is
|
||||
# spelled with an environment variable and a ctest entry of its own.
|
||||
#
|
||||
# Note the shape of every list here: it APPENDS to MGL_ITEST_COMMON_ENV /
|
||||
# MGL_ITEST_VULKAN_ENV rather than standing alone. A ctest ENVIRONMENT property REPLACES
|
||||
# the job environment rather than adding to it, so an entry that lists only its mode
|
||||
# variable would silently lose the EGL vendor and Vulkan ICD pinning and run against
|
||||
# whatever the loader found first.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
# MOBILEGL_LOG_FILE_PATH alongside the pin, because the arming assertion needs somewhere to
|
||||
# read the library's own report from. The strip's arming signal is a latched MGLOG_I and there
|
||||
# is no other way for a test process to learn that it fired - MG_Config is not reachable from
|
||||
# this module on Android, where it links the shipping library. The path is per-lane so nothing
|
||||
# else appends to it, and the case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
|
||||
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
|
||||
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
|
||||
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
|
||||
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
|
||||
# SPIR-V Xfb decorations to the carrier).
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
|
||||
@@ -367,3 +452,181 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
|
||||
# reason the depth/stencil entry above pins its emulation: without it this scenario is
|
||||
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
|
||||
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
|
||||
# emulation never runs - the ambient registration would be exercising the un-stripped path
|
||||
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
|
||||
# location and the assertion is about the spelling the device gets.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
|
||||
TEST_FILTER "UnlocatedIoBlockScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||
#
|
||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||
# MobileGL's built-in default happens to be, and the day that default flips they would
|
||||
# stop covering the asynchronous path without anything going red. These entries are the
|
||||
# ones that keep the asynchronous half tested no matter what ships. They are also the
|
||||
# only place ExtensionStringMatchesTheConfiguration can assert that the extension IS
|
||||
# advertised - the case derives its expectation from this variable and nothing else, and
|
||||
# skips where it is unset, precisely so that it is not asserting the implementation
|
||||
# against itself.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The other side of the same switch: asynchronous compilation OFF, so
|
||||
# GL_KHR_parallel_shader_compile must be WITHDRAWN from both spellings of the extension
|
||||
# list and GL_MAX_SHADER_COMPILER_THREADS_KHR must read 0. Only that one case is
|
||||
# registered here because it is the only one that has anything to say in this
|
||||
# configuration - the other four exist to observe worker-built artifacts, and there are
|
||||
# none - so registering the whole scenario would buy four guaranteed skips per backend.
|
||||
# Together with the AsyncOn. entries above, one ctest run still covers both flag states,
|
||||
# which is what the in-process forcing used to be for.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The optimistic-status quirk's end-to-end shape. Its own entries and not part of the
|
||||
# AsyncOn. ones because the quirk is not neutral for the rest of the scenario: with it in
|
||||
# force glGetShaderiv(GL_COMPILE_STATUS) deliberately answers without joining, which is
|
||||
# exactly what CompletionStatusPollingThenForcedJoin asserts must NOT happen. Off by
|
||||
# default and never advertised, so - unlike asynchronous compilation, which announces
|
||||
# itself through the extension string - the variable is the only thing that can tell the
|
||||
# case it is in force.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The negative control for the DirectGLES gl_ViewportIndex emulation, in a process that
|
||||
# has it switched off. One case, because it is the only one the switch may touch: with
|
||||
# the emulation off the three positive cases in the same fixture describe behaviour the
|
||||
# backend does not have, so a whole-scenario registration would be three guaranteed reds.
|
||||
# DirectGLES only - the flag steers nothing on DirectVulkan, which routes natively.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.NoViewportArrayEmulation."
|
||||
TEST_FILTER "ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
|
||||
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
|
||||
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
|
||||
# the same GL questions and must produce the same numbers - the "two pools must agree"
|
||||
# gate this machine can hold that the affected device cannot. The pinned entry is also
|
||||
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
|
||||
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
|
||||
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
|
||||
# steers nothing on DirectGLES.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PrimGenReroute."
|
||||
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
|
||||
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
|
||||
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
|
||||
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
|
||||
# DirectVulkan, which has always stored these formats widened.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.WidenedPacked16."
|
||||
TEST_FILTER "CopyImagePacked16Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
|
||||
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
|
||||
# geometry stages, so the ambient registrations run these captures through the built-in and
|
||||
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
|
||||
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
|
||||
# path. Both backends, because the demotion is shared phase-B work with two different
|
||||
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -552,7 +552,15 @@ namespace MGITest {
|
||||
// before the pre-flight forks - the child must measure the same platform
|
||||
// the parent will use.
|
||||
EnsureHeadlessPlatform();
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||
// The backend that is actually about to come up, which is what every
|
||||
// `BackendName() == "DirectGLES"` gate in the scenarios means by the question.
|
||||
// MG_ConfigLoader::InitBackendType defaults an unset MOBILEGL_BACKEND_TYPE to
|
||||
// DirectGLES, so the same default belongs here; this used to report the literal
|
||||
// "<unset>" instead. Under ctest the variable is always set by the ENVIRONMENT
|
||||
// property, which is why that never showed - but run straight from a device
|
||||
// shell, where nothing sets it, DirectGLES came up and every case gated on the
|
||||
// NAME DirectGLES skipped as though it had not.
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "DirectGLES");
|
||||
m_usable = BringUp();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,49 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "HeadlessGL.h"
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// How a MOBILEGL_* quirk variable reads in THIS process's environment.
|
||||
//
|
||||
// A scenario that needs a non-default configuration takes it from here and skips
|
||||
// when the process it was launched into is not in that configuration, rather than
|
||||
// writing MG_Config::Features itself. Two reasons, and the second one decides it:
|
||||
//
|
||||
// - the feature table is an internal symbol. On Android this module links against
|
||||
// the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the
|
||||
// real artifact - and that library is built -fvisibility=hidden, so nothing
|
||||
// internal is reachable from here at all.
|
||||
// - a quirk poked in-process is already too late for everything latched at
|
||||
// initialization: the compile pool and its threads, and the backend's advertised
|
||||
// extension list, which is built once from the configuration in force at first
|
||||
// use. The process-wide variable is the only spelling that covers the whole
|
||||
// configuration instead of the half of it that is still mutable afterwards.
|
||||
//
|
||||
// The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp,
|
||||
// QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a
|
||||
// built-in default, i.e. a value only the implementation knows - a truthy value is
|
||||
// On, and anything else that IS set ("0", "false", "") is Off.
|
||||
enum class AmbientQuirk { Auto, On, Off };
|
||||
|
||||
inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr) return AmbientQuirk::Auto;
|
||||
std::string lowered(value);
|
||||
for (char& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
|
||||
return AmbientQuirk::On;
|
||||
}
|
||||
|
||||
class ScenarioTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
|
||||
@@ -56,7 +56,13 @@ namespace MGITest {
|
||||
|
||||
const std::vector<LimitBound>& BufferLimitTable() {
|
||||
static const std::vector<LimitBound> table = {
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
||||
// 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
|
||||
// layer's indexed-binding array - the two were made to coincide when the array
|
||||
// was widened from 36, which had made the clamp in GL_Getter degenerate.
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
|
||||
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
|
||||
// three, and the two tessellation stages were simply missing from it.
|
||||
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
@@ -133,6 +139,49 @@ namespace MGITest {
|
||||
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
|
||||
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
|
||||
}
|
||||
|
||||
// THE MIDDLE TERM, which the relation quoted above always had and this case never
|
||||
// checked. It is the one that actually broke: widening the binding-point array to 84
|
||||
// raised what every PER-STAGE count clamps to, while the combined value was a
|
||||
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
|
||||
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
|
||||
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
|
||||
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
|
||||
struct StageAgainstCombined {
|
||||
GLenum stage;
|
||||
const char* stageName;
|
||||
GLenum combined;
|
||||
const char* combinedName;
|
||||
};
|
||||
const StageAgainstCombined stageRelations[] = {
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
};
|
||||
for (const StageAgainstCombined& relation : stageRelations) {
|
||||
GLint stage = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(relation.stage, &stage);
|
||||
glGetIntegerv(relation.combined, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
|
||||
EXPECT_LE(stage, combined)
|
||||
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
|
||||
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
|
||||
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
|
||||
@@ -199,6 +248,80 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
|
||||
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
|
||||
// too-small value against a context advertising 4.6, and each is the FIRST call its
|
||||
// conformance case makes - so the case died before it could measure anything.
|
||||
//
|
||||
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
|
||||
// cull-distance route MUST report it), so it is checked for answerability only, below.
|
||||
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
|
||||
const std::vector<LimitBound> table = {
|
||||
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
|
||||
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
|
||||
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
|
||||
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
|
||||
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
|
||||
// the number un-gates two transform-feedback CTS cases per package across
|
||||
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
|
||||
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
|
||||
// a row here would pin a number the implementation cannot back.
|
||||
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
|
||||
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
|
||||
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
|
||||
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
|
||||
58368, 1 << 30},
|
||||
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
|
||||
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
|
||||
};
|
||||
for (const LimitBound& bound : table) {
|
||||
GLint value = -424242;
|
||||
glGetIntegerv(bound.pname, &value);
|
||||
const unsigned int error = FirstGLError();
|
||||
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
|
||||
if (error != GL_NO_ERROR) continue;
|
||||
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
|
||||
<< bound.minimum;
|
||||
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
|
||||
<< bound.ceiling;
|
||||
}
|
||||
|
||||
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
|
||||
// so only answerability and the combined-limit ordering are checked here.
|
||||
GLint cull = -1;
|
||||
GLint clip = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
|
||||
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
|
||||
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
|
||||
EXPECT_GE(cull, 0);
|
||||
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
|
||||
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
|
||||
|
||||
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
|
||||
// the wide query must answer it and the narrow one must saturate rather than wrap.
|
||||
GLint64 elementIndex = -1;
|
||||
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
|
||||
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
// be able to turn this into a red.
|
||||
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
||||
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
|
||||
// (c) The extension string matches the configuration. This is the half a recorded
|
||||
// trace can never cover - Iris and Sodium change their submission schedule the
|
||||
// moment they see the string - so it is asserted against a real backend's real
|
||||
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
||||
// (c) The extension string matches the configuration - where "the configuration" is
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the
|
||||
// implementation says about itself. This is the half a recorded trace can never
|
||||
// cover - Iris and Sodium change their submission schedule the moment they see the
|
||||
// string - so it is asserted against a real backend's real GL_EXTENSIONS, through
|
||||
// both glGetString and glGetStringi.
|
||||
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||
// is synchronous. That is what the extension requires of a zero count.
|
||||
@@ -40,6 +42,27 @@
|
||||
//
|
||||
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||
// so this file runs twice per ctest invocation.
|
||||
//
|
||||
// COMPILATION MODE IS PER PROCESS TOO. Every case here needs a particular configuration of
|
||||
// MobileGL's shader compiler, and takes it from the ENVIRONMENT
|
||||
// (MOBILEGL_ASYNC_SHADER_COMPILE, MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS) rather than by
|
||||
// writing MG_Config::Features on the way past. Half of what those variables decide is
|
||||
// latched before the first GL call - the compile pool and its threads, and the advertised
|
||||
// extension list a backend builds once from the configuration in force at its first use -
|
||||
// so an in-process poke could only ever have moved the other half; and on Android it could
|
||||
// move nothing at all, because this module links against the shipping libMobileGL.so, which
|
||||
// exports no such symbol. A case whose process is not in the configuration it needs SKIPS
|
||||
// with that as its reason. CMakeLists.txt registers the extra ctest entries that put a
|
||||
// process into each configuration (AsyncOn., AsyncOff., OptimisticShaderStatus.), so one
|
||||
// ctest run still covers both sides of every switch. Run straight from a shell with nothing
|
||||
// set - the on-device shape - the ambient configuration runs and the rest skip cleanly.
|
||||
//
|
||||
// WITHIN one process, "compiled on a worker" versus "compiled on this thread" is switched
|
||||
// through glMaxShaderCompilerThreadsKHR, the extension's own entry point: a zero count joins
|
||||
// everything outstanding and compiles inline from then on, any nonzero count lifts that
|
||||
// again, and 0xFFFFFFFF asks for the implementation maximum (GL_Program.cpp,
|
||||
// MaxShaderCompilerThreadsKHR_State). Doing it through the public call rather than the
|
||||
// feature table means the switching is itself part of what these cases exercise.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -47,9 +70,6 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
using MobileGL::MG_Config::QuirkOverride;
|
||||
|
||||
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
||||
// thing that can differ between the two compilation modes is the compilation.
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
@@ -139,50 +157,40 @@ void main() {
|
||||
return source;
|
||||
}
|
||||
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
||||
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
||||
// it in-process is what lets ONE ctest run compare the two modes against each
|
||||
// other - the whole point of (e).
|
||||
class AsyncModeScope {
|
||||
public:
|
||||
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
||||
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
||||
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
||||
// Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly
|
||||
// "MobileGL is configured to compile asynchronously" as an application can see it:
|
||||
// the backends gate the string on AsyncShaderCompileEnabled() and on nothing else
|
||||
// (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string
|
||||
// is the only way MobileGL ever tells anyone. A case that needs asynchronous
|
||||
// compilation checks for it the way an application would, and skips without it.
|
||||
//
|
||||
// The INDEXED form, because that is the one a core-profile application reads.
|
||||
bool HasParallelShaderCompile() {
|
||||
GLint count = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") return true;
|
||||
}
|
||||
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||
AsyncModeScope(const AsyncModeScope&) = delete;
|
||||
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
||||
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
||||
class OptimisticStatusScope {
|
||||
public:
|
||||
explicit OptimisticStatusScope(const QuirkOverride mode)
|
||||
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||
}
|
||||
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it
|
||||
// has to put the pool back or it changes how every scenario after it compiles.
|
||||
//
|
||||
// The restore is the extension's own "implementation maximum" spelling rather than a
|
||||
// hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined
|
||||
// (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State) as precisely the two steps this
|
||||
// used to perform through internal entry points - concurrency := the pool's full
|
||||
// thread count, then lift any suspension a zero count had armed - in the safer order,
|
||||
// since it raises the budget before re-admitting work rather than after. Going through
|
||||
// the public call also puts the restore path itself under test, and it is the only
|
||||
// spelling available on Android, where this module links the shipping shared library
|
||||
// and can reach nothing but the GL entry points.
|
||||
class CompilerThreadScope {
|
||||
public:
|
||||
CompilerThreadScope() = default;
|
||||
~CompilerThreadScope() {
|
||||
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
||||
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
||||
pool.SetMaxConcurrency(pool.GetThreadCount());
|
||||
}
|
||||
~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); }
|
||||
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||
};
|
||||
@@ -293,7 +301,12 @@ void main() {
|
||||
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so no compile can be "
|
||||
"outstanding; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
// One worker, so the queue behind it is what the poll observes.
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
@@ -341,14 +354,33 @@ void main() {
|
||||
}
|
||||
|
||||
// ---- (c) ------------------------------------------------------------------
|
||||
// The extension string, read from a real backend that really brought a driver
|
||||
// up. No mode forcing here: a backend builds its advertised list once, from the
|
||||
// configuration in force at its first use, so the meaningful assertion is
|
||||
// against the AMBIENT configuration - which is exactly what makes this case
|
||||
// worth running in both of the suite's flag states.
|
||||
// The extension string, read from a real backend that really brought a driver up.
|
||||
//
|
||||
// The expectation comes from the ENVIRONMENT, never from the implementation. This
|
||||
// case used to derive it by calling AsyncShaderCompileEnabled() - which is the same
|
||||
// function the backends gate the string on, so the two halves could only ever agree
|
||||
// and the case would have passed however wrong both of them were. Asserting an
|
||||
// implementation against itself pins nothing.
|
||||
//
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE is the whole input: the process inherited it before
|
||||
// any GL call, a backend builds its advertised list once from the configuration in
|
||||
// force at first use, and nothing in this process can move it afterwards. So reading
|
||||
// the variable IS reading the configuration, independently. With the variable unset
|
||||
// the configuration in force is MobileGL's built-in default, which only the
|
||||
// implementation knows - there is nothing independent left to compare against, and
|
||||
// this case says so rather than inventing an expectation. The AsyncOn. and AsyncOff.
|
||||
// ctest entries pin the variable to each of its two values, so one ctest run still
|
||||
// asserts both the advertised and the withdrawn side.
|
||||
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||
if (!Ready()) return;
|
||||
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
|
||||
const AmbientQuirk configured = AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
if (configured == AmbientQuirk::Auto) {
|
||||
GTEST_SKIP() << "MOBILEGL_ASYNC_SHADER_COMPILE is unset, so the configuration in force is "
|
||||
"MobileGL's built-in default and the only way to learn it would be to ask "
|
||||
"the implementation this case exists to check; the AsyncOn. and AsyncOff. "
|
||||
"ctest entries run it with the variable pinned to each of its two values";
|
||||
}
|
||||
const bool expected = configured == AmbientQuirk::On;
|
||||
|
||||
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
ASSERT_NE(extensions, nullptr);
|
||||
@@ -385,7 +417,12 @@ void main() {
|
||||
// A zero count must leave nothing in flight and keep it that way.
|
||||
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so a zero count has "
|
||||
"nothing to settle; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
@@ -417,12 +454,28 @@ void main() {
|
||||
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||
// backend's orientation and present path live, so the comparison covers the
|
||||
// whole pipeline rather than the reflection tables alone.
|
||||
//
|
||||
// The two modes are selected through glMaxShaderCompilerThreadsKHR, the extension's
|
||||
// own entry point, rather than through the feature table: a zero count joins
|
||||
// everything outstanding and makes every later glCompileShader/glLinkProgram run its
|
||||
// body on the calling thread, and 0xFFFFFFFF lifts that again with the pool at its
|
||||
// full thread count (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State; the compile
|
||||
// and link paths both gate on AsyncShaderCompileActive(), which is what the zero
|
||||
// count switches). So this is still one process comparing worker-built artifacts
|
||||
// against inline-built ones - just asked for the way an application asks.
|
||||
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so both halves would "
|
||||
"be the same inline build and the comparison would be vacuous; the "
|
||||
"AsyncOn. ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
Image asyncImage;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
asyncImage = DrawFrameWith(program);
|
||||
@@ -431,7 +484,7 @@ void main() {
|
||||
|
||||
Image syncImage;
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
glMaxShaderCompilerThreadsKHR(0);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
syncImage = DrawFrameWith(program);
|
||||
@@ -456,11 +509,16 @@ void main() {
|
||||
// candidate) shows up here and not in the single-program case above.
|
||||
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so nothing would be "
|
||||
"built on a worker and there is no per-worker state to leak; the AsyncOn. "
|
||||
"ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
// Everything enqueued before anything is read: the only shape in which
|
||||
@@ -489,6 +547,21 @@ void main() {
|
||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||
if (!Ready()) return;
|
||||
// The quirk is off by default and never advertised, so unlike the cases above
|
||||
// there is no GL observable that says whether it is in force - only the variable
|
||||
// that put it there. It also has to be set BEFORE this process started for the
|
||||
// shape to be the real one: the optimistic answer is latched per compile, and a
|
||||
// quirk switched on mid-process would only cover the compiles after it.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case is the optimistic-status quirk's end-to-end shape and needs it on "
|
||||
"for the whole process; the OptimisticShaderStatus. ctest entries run it with "
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1";
|
||||
}
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "the optimistic status only ever applies to a compile that is still in flight "
|
||||
"(OptimisticShaderStatusActive() requires AsyncShaderCompileActive()), and "
|
||||
"this process is configured to compile inline";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||
@@ -508,8 +581,6 @@ void main() {
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
|
||||
@@ -87,11 +87,6 @@ void main() {
|
||||
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
|
||||
<< "; this needs 3 and 2";
|
||||
}
|
||||
if (!AtomicCountersAreWired()) {
|
||||
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
|
||||
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
|
||||
<< "is still resolved from the shader-storage binding points";
|
||||
}
|
||||
m_program = CompileComputeProgram(kCounterComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
}
|
||||
@@ -105,12 +100,6 @@ void main() {
|
||||
m_program = 0;
|
||||
}
|
||||
|
||||
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
|
||||
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
|
||||
// frontend half (limits, reflection queries, the link-time offset rules) is
|
||||
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
|
||||
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
@@ -236,4 +225,43 @@ void main() {
|
||||
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
||||
}
|
||||
|
||||
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
||||
// between. Each backend has its own way to invert this pair, and both are pinned here.
|
||||
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
|
||||
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
|
||||
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
|
||||
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
|
||||
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
|
||||
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
|
||||
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
|
||||
// the deferred frame command buffer. Unless the frontend retires that pending work before
|
||||
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
|
||||
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
|
||||
// pins the other direction for both: the upload must leave bytes outside its range - the
|
||||
// dispatch's results - untouched.
|
||||
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||
MakeCounterBuffer(1, {0u});
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
Dispatch();
|
||||
|
||||
const unsigned int reseed = 4242u;
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
|
||||
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), &reseed);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
|
||||
|
||||
const std::vector<unsigned int> values = ReadCounters(zero, 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(values[0], reseed)
|
||||
<< "offset 0 read back " << values[0] << "; the dispatch's value (" << kInvocations
|
||||
<< ") means the readback ran before the queued SubData range was flushed and reverted it";
|
||||
EXPECT_EQ(values[1], 2 * kInvocations)
|
||||
<< "offset 4 read back " << values[1] << "; the SubData flush must leave bytes outside its "
|
||||
<< "range untouched";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.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 - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED.
|
||||
//
|
||||
// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE
|
||||
// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level
|
||||
// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with
|
||||
// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no
|
||||
// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body
|
||||
// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and
|
||||
// all three entry points.
|
||||
//
|
||||
// The frontend understands this shape - the clear is a pure CPU-shadow write, and
|
||||
// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The
|
||||
// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a
|
||||
// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and
|
||||
// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly
|
||||
// as it found it. The conformance failures carried no <Text> at all, because nothing raised a GL
|
||||
// error: the destination was simply never written, so the test compared its own zero-initialized
|
||||
// buffer against the clear value.
|
||||
//
|
||||
// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have
|
||||
// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on
|
||||
// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would
|
||||
// return stale bytes for every render-to-texture result instead.
|
||||
//
|
||||
// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible
|
||||
// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a
|
||||
// separate change (it moves every GL-level-to-subresource translation in the backend); this
|
||||
// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able
|
||||
// to define a lone level N - is the built-in control for what the answer should be.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#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 {
|
||||
|
||||
// The conformance family's own shape: a mid-chain level of a texture that has nothing else.
|
||||
constexpr GLint kDefinedLevel = 3;
|
||||
constexpr GLsizei kLevelExtent = 8;
|
||||
|
||||
struct Texel8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
bool operator==(const Texel8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Texel8& c) {
|
||||
return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||
}
|
||||
|
||||
// The conformance test's clear value is a single repeated component; 5 is what it uses, and
|
||||
// it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one).
|
||||
constexpr Texel8 kClearValue{5, 5, 5, 5};
|
||||
constexpr Texel8 kInitialValue{200, 100, 50, 255};
|
||||
|
||||
class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_texture != 0) {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, &m_texture);
|
||||
m_texture = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D,
|
||||
// which would define the whole chain and could not express "level 0 does not exist".
|
||||
void MakeTextureWithOnlyLevel(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> initial(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
initial.data());
|
||||
// What the conformance case does: MAX_LEVEL names the one level that exists, and
|
||||
// BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND
|
||||
// nominally the base level, the shape the backend could not express.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level;
|
||||
}
|
||||
|
||||
std::vector<Texel8> ReadLevel(GLint level) {
|
||||
std::vector<Texel8> pixels(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0});
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind";
|
||||
return pixels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, const std::vector<Texel8>& pixels, Texel8 expected) {
|
||||
std::size_t offenders = 0;
|
||||
Texel8 firstBad{};
|
||||
for (const Texel8& pixel : pixels) {
|
||||
if (pixel == expected) continue;
|
||||
if (offenders == 0) firstBad = pixel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " ("
|
||||
<< offenders << " of " << pixels.size() << " texels wrong)";
|
||||
}
|
||||
|
||||
// Level 0 defined, a GAP, then `level` defined. GL keeps the intervening levels at a zero
|
||||
// extent, so the backend's mip walk stops at the gap and the VkImage ends up with FEWER
|
||||
// mip levels than the GL level count - which is a different shape from "no image at all"
|
||||
// and is why the readback has to bound the level against the IMAGE.
|
||||
void MakeTextureWithAGapBefore(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> base(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
base.data());
|
||||
const std::vector<Texel8> gapped(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, gapped.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with a gap before level " << level;
|
||||
}
|
||||
|
||||
GLuint m_texture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the
|
||||
// caller's buffer kept whatever it already held - which is why the conformance failures showed
|
||||
// the test's own zero-initialized memory and carried no GL error.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
// Pre-flight: the level reads back as what was uploaded. This is what makes the assertion
|
||||
// after the clear falsifiable - without it, a readback that silently wrote nothing could not
|
||||
// be told from one that wrote the right answer.
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same shape through glClearTexSubImage, which is a separate entry point in the conformance
|
||||
// family and failed on exactly the same bodies.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from
|
||||
// the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally
|
||||
// would pass the two tests above and this one too - but it would also hand back stale bytes for
|
||||
// anything the GPU had written, which is why the partial-clear check below matters: the readback
|
||||
// has to see a region the backend cleared and a region it did not, in one image.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(0);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue);
|
||||
|
||||
// Clear only the left half, so the answer is neither "all initial" nor "all cleared".
|
||||
glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
&kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
const std::vector<Texel8> pixels = ReadLevel(0);
|
||||
ASSERT_EQ(pixels.size(), static_cast<std::size_t>(kLevelExtent) * kLevelExtent);
|
||||
for (int y = 0; y < kLevelExtent; ++y) {
|
||||
for (int x = 0; x < kLevelExtent; ++x) {
|
||||
const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue;
|
||||
const Texel8 actual = pixels[static_cast<std::size_t>(y) * kLevelExtent + x];
|
||||
ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")";
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The adjacent shape the first fix did NOT cover: level 0 defined, a gap, then the level being
|
||||
// read. This one DOES get a VkImage - just one with fewer mip levels than GL thinks the texture
|
||||
// has - so the "no VkImage" test passes and the GL level was written straight into
|
||||
// imageSubresource.mipLevel and into a VkImageMemoryBarrier's baseMipLevel. An out-of-range
|
||||
// subresource is a promise the driver takes at face value; the glCopyImageSubData path two
|
||||
// functions away grew the same guard after it SIGSEGV'd inside the Adreno driver.
|
||||
//
|
||||
// The level being read really does hold its own data (the shadow is its only copy, since nothing
|
||||
// ever uploaded it), so the correct answer is the uploaded bytes - not a decline.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadBackALevelSeparatedFromLevelZeroByAGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithAGapBefore(kDefinedLevel);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
|
||||
// Level 0 is backed by the real image and must still read back from it, so the level bound is
|
||||
// about the level and not about the texture.
|
||||
ExpectAllTexels("level 0 after clearing level 3", ReadLevel(0), kInitialValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,375 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.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 - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
|
||||
//
|
||||
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
|
||||
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
|
||||
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
|
||||
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
|
||||
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
|
||||
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
|
||||
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
|
||||
// texel-block move - between a mirrored allocation and a plain one lands the fields
|
||||
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
|
||||
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
|
||||
// image are clean (the driver decodes its own layout consistently), which is why only the
|
||||
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
|
||||
// checks always passed.
|
||||
//
|
||||
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
|
||||
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
|
||||
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
|
||||
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
|
||||
// the driver's layout.
|
||||
//
|
||||
// The repair under test is the packed16 storage widening
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
|
||||
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
|
||||
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
|
||||
// encodes to UNorm8 and back losslessly for every n <= 8.
|
||||
//
|
||||
// This scenario runs in BOTH configurations, and both must hand back identical client words:
|
||||
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
|
||||
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
|
||||
// covered);
|
||||
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
|
||||
// which is the storage every affected device will actually run - without it the repair
|
||||
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
|
||||
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
|
||||
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
|
||||
// client-invisible: same words in, same words out, on every leg the failing bodies used.
|
||||
//
|
||||
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
|
||||
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
|
||||
// means the widening (or the narrow path it replaces) is.
|
||||
|
||||
#include <algorithm>
|
||||
#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 kBaseSize = 30; // array level 0; level 1 is 15x15
|
||||
constexpr int kLevel1Size = kBaseSize / 2;
|
||||
constexpr int kLayers = 12;
|
||||
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
|
||||
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
|
||||
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
|
||||
constexpr int kRegion = kFlatSize;
|
||||
constexpr int kArrayOffset = 8;
|
||||
|
||||
struct PackedFormatCase {
|
||||
GLenum internalFormat; // the spelling the CTS uses
|
||||
GLenum transferFormat;
|
||||
GLenum transferType;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
// Per-texel varying words, every field inside its width, so a swapped field order (or
|
||||
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
|
||||
GLushort MakeWord(GLenum type, int i) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
|
||||
return static_cast<GLushort>((r << 11) | (g << 5) | b);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4: {
|
||||
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
|
||||
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
|
||||
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
|
||||
std::vector<GLushort> words(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
class CopyImagePacked16Scenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
|
||||
// every odd-width row of the 15x15 level and shear the comparisons.
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 2);
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
|
||||
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
|
||||
// below can pass by reading a level that was never written.
|
||||
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
|
||||
const std::vector<GLushort>& level1) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
|
||||
kLayers, 0, format.transferFormat, format.transferType, level0.data());
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
|
||||
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
|
||||
const int level2Size = kLevel1Size / 2;
|
||||
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
|
||||
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
|
||||
// every assertion reads.
|
||||
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(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);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
|
||||
format.transferFormat, format.transferType, texels.data());
|
||||
for (int level = 1; level <= 2; ++level) {
|
||||
const int size = std::max(kFlatSize >> level, 1);
|
||||
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
|
||||
format.transferFormat, format.transferType, fill.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
|
||||
const PackedFormatCase& format, size_t texelCount) {
|
||||
std::vector<GLushort> words(texelCount, 0);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
|
||||
glBindTexture(target, 0);
|
||||
return words;
|
||||
}
|
||||
|
||||
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
|
||||
// image equals the corresponding source word, and every word outside it still holds
|
||||
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
|
||||
// regression into a one-line diagnosis.
|
||||
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
|
||||
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
|
||||
const std::vector<GLushort>& fill, const char* what) {
|
||||
for (int y = 0; y < width; ++y) {
|
||||
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
|
||||
const bool inRegion =
|
||||
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
|
||||
const GLushort actual = got[static_cast<size_t>(y * width + x)];
|
||||
const GLushort expected =
|
||||
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
|
||||
(x - x0))]
|
||||
: fill[static_cast<size_t>(y * width + x)];
|
||||
EXPECT_EQ(actual, expected)
|
||||
<< what << ": texel (" << x << ", " << y << ")"
|
||||
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
|
||||
<< ", expected 0x" << expected;
|
||||
if (actual != expected) return; // one texel names the defect; 224 more would bury it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
const PackedFormatCase kFormats[] = {
|
||||
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
|
||||
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
|
||||
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
|
||||
};
|
||||
|
||||
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
|
||||
// the array-as-destination direction of 12 of the 18 failing bodies.
|
||||
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint source = MakeFlatTexture(format, flat);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
|
||||
kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
|
||||
(std::string("2d->2d_array level 1, ") + format.name).c_str());
|
||||
// The source must not have moved - the CTS asserts this before it ever looks at
|
||||
// the destination, and it is what pins the corruption to the copy itself.
|
||||
const auto sourceAfter =
|
||||
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
|
||||
(std::string("source after 2d->2d_array, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
|
||||
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
|
||||
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint destination = MakeFlatTexture(format, fill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
|
||||
static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
|
||||
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
|
||||
// that requires the renderbuffer's ES storage to move together with the textures' -
|
||||
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
|
||||
// reached textures alone would break exactly here.
|
||||
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
|
||||
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< format.name << ": the renderbuffer is not attachable";
|
||||
// Field values picked to encode exactly in the narrow fields AND in their
|
||||
// UNorm8 expansions, so the expected word is the same whichever storage the
|
||||
// configuration picked - which is the point of the whole scenario.
|
||||
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
|
||||
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
|
||||
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
|
||||
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
|
||||
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
|
||||
static_cast<GLfloat>(max - 2) / max, 1.0f};
|
||||
// The context is shared with every scenario in this process; a scissor left on
|
||||
// would clip the clear and hand the copy undefined renderbuffer texels.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearBufferfv(GL_COLOR, 0, clearColor);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
|
||||
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
GLushort clearedWord = 0;
|
||||
switch (format.transferType) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4:
|
||||
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
|
||||
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,238 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.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 DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
|
||||
//
|
||||
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
|
||||
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
|
||||
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
|
||||
// the capability IS there both backends translate the factors properly, and that has always
|
||||
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
|
||||
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
|
||||
// application asking for a blend factor the device cannot do is a picture problem, never a reason
|
||||
// to take the process down.
|
||||
//
|
||||
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
|
||||
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
|
||||
// scenario pins that it lands on one of them and never on a crash:
|
||||
//
|
||||
// capability present - src0 * src1 + dst * (1 - src1)
|
||||
// capability absent - src0, written straight through
|
||||
//
|
||||
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
|
||||
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
|
||||
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
|
||||
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
|
||||
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
|
||||
// scenario exists for - is unit-tested against stubbed capabilities in
|
||||
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
|
||||
// which is the only place it can be reached without a driver that lacks the extension.
|
||||
//
|
||||
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
|
||||
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
|
||||
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
|
||||
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
|
||||
// would still be invalid pipeline state.
|
||||
|
||||
#include <cstdint>
|
||||
#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 int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
|
||||
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
|
||||
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
|
||||
// harness's compile-and-link helper.
|
||||
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uSrc0;
|
||||
uniform vec4 uSrc1;
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main()
|
||||
{
|
||||
fragColor0 = uSrc0;
|
||||
fragColor1 = uSrc1;
|
||||
}
|
||||
)";
|
||||
|
||||
class DualSourceBlendScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
|
||||
m_programError = error;
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// Capability probe, not an assertion. A GL link that succeeded is not proof that
|
||||
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
|
||||
// use, and GLSL ES has no `index` layout qualifier outside
|
||||
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
|
||||
// and the draw renders nothing. One unblended white draw tells the two apart, and
|
||||
// the cases skip rather than measure a picture the driver never produced.
|
||||
if (m_program != 0) {
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
|
||||
glFinish();
|
||||
const Image probe = ReadPixels(kExtent, kExtent);
|
||||
m_programRenders =
|
||||
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
|
||||
}
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
}
|
||||
|
||||
void Draw(float src0, float src1) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
// Both cases share this gate: nothing below can be measured on a backend that cannot
|
||||
// run a dual-source fragment program at all. Returns the skip reason, empty when the
|
||||
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
|
||||
// expands to a `return` and would leave only the HELPER, letting the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyTheProgramCannotRun() const {
|
||||
if (m_program == 0) {
|
||||
return "this driver cannot build a dual-source fragment shader: " + m_programError;
|
||||
}
|
||||
if (!m_programRenders) {
|
||||
return "this backend links a dual-source fragment program but renders nothing with it "
|
||||
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
bool m_programRenders = false;
|
||||
std::string m_programError;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The whole point of the scenario: this sequence used to be a process kill on any device
|
||||
// without the capability, and it has to be a picture either way.
|
||||
//
|
||||
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
|
||||
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
|
||||
// declined = 1.0 -> 255
|
||||
// Anything else means the factors were mistranslated rather than either honoured or declined.
|
||||
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
|
||||
glFinish();
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
const int red = static_cast<int>(centre.r);
|
||||
const bool blended = red > 100 && red < 160;
|
||||
const bool declined = red > 245;
|
||||
EXPECT_TRUE(blended || declined)
|
||||
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
|
||||
<< "straight-through source (255) - the SRC1 factors were mistranslated";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
|
||||
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
|
||||
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
|
||||
// source either way, so this case is really "no crash, no error, no surprise".
|
||||
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(static_cast<int>(centre.r), 245)
|
||||
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,794 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GuiBatchScenario.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 - A GUI QUAD DRAWN THE WAY AcceleratedRendering DRAWS ONE.
|
||||
//
|
||||
// The mod replaces every GUI blit with a compute pass: the application writes unit-space
|
||||
// vertices into a persistently mapped SSBO, a compute shader multiplies them by a shared
|
||||
// transform into a second buffer, and that second buffer is then bound as GL_ARRAY_BUFFER of a
|
||||
// DSA vertex array and drawn with glDrawElementsBaseVertex through vanilla's position_tex_color
|
||||
// program. Every element of that is replayed here, one axis per test, so a failure names the
|
||||
// element that killed the quad rather than "the GUI is broken".
|
||||
//
|
||||
// The axis that mattered is MeshesBlockLeftUnbound. The mod's vertex-transform compute shader
|
||||
// declares SIX storage blocks, and the last of them - `Meshes`, the cache of pre-uploaded model
|
||||
// geometry - is read only when a vertex says it comes from a cached mesh. A batch of plain GUI
|
||||
// blits has no cached meshes, so the mod binds nothing at that point and the shader never reads
|
||||
// it. GL 4.6 core 7.8 is explicit that this is legal: a storage block with no buffer at its
|
||||
// binding point simply has no store.
|
||||
//
|
||||
// DirectVulkan used to refuse the whole descriptor set over it, and both SetupDraw and
|
||||
// DispatchCompute skip their work on that refusal - so the transform dispatch never ran, the
|
||||
// output vertex buffer kept whatever was in it, and every hotbar and container-screen background
|
||||
// quad came out degenerate. Items were unaffected because item geometry DOES come from cached
|
||||
// meshes, which is what made the bug look like "only the backgrounds disappear".
|
||||
//
|
||||
// The assertions are whole-region, not centre-pixel: a quad that survives with three stale
|
||||
// vertices still paints its centre.
|
||||
//
|
||||
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound binding to the GLES driver,
|
||||
// which does what GL says, so it is the control - every test here must stay green on both.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTransformComputeSource = R"(#version 460 core
|
||||
|
||||
struct Vertex {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
float u0;
|
||||
float v0;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct VaryingData {
|
||||
int offset;
|
||||
int sharing;
|
||||
int mesh;
|
||||
int shouldCull;
|
||||
};
|
||||
|
||||
struct SharingData {
|
||||
mat4 transform;
|
||||
mat3 normal;
|
||||
};
|
||||
|
||||
layout(local_size_x = 128) in;
|
||||
|
||||
layout(binding=0, std430) restrict readonly buffer VerticesIn {
|
||||
Vertex verticesIn[];
|
||||
};
|
||||
|
||||
layout(binding=1, std430) restrict writeonly buffer VerticesOut {
|
||||
Vertex verticesOut[];
|
||||
};
|
||||
|
||||
layout(binding=2, std430) restrict readonly buffer Sharings {
|
||||
SharingData sharings[];
|
||||
};
|
||||
|
||||
layout(binding=3, std430) restrict readonly buffer VaryingsIn {
|
||||
VaryingData varyingsIn[];
|
||||
};
|
||||
|
||||
layout(binding=4, std430) restrict writeonly buffer VaryingsOut {
|
||||
VaryingData varyingsOut[];
|
||||
};
|
||||
|
||||
layout(binding=5, std430) restrict readonly buffer Meshes {
|
||||
Vertex meshVertices[];
|
||||
};
|
||||
|
||||
layout(location=0) uniform uint vertexCount;
|
||||
layout(location=1) uniform uint vertexOffset;
|
||||
layout(location=2) uniform uint varyingOffset;
|
||||
|
||||
void main() {
|
||||
uint indexIn = gl_GlobalInvocationID.x;
|
||||
uint vertexOut = indexIn + vertexOffset;
|
||||
uint varyingOut = indexIn + varyingOffset;
|
||||
|
||||
if (indexIn >= vertexCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
int offset = varyingsIn[indexIn] .offset;
|
||||
uint reference = indexIn - offset;
|
||||
int sharing = varyingsIn[reference] .sharing;
|
||||
int mesh = varyingsIn[reference] .mesh;
|
||||
|
||||
mat4 transformMatrix;
|
||||
|
||||
if (sharing != -1) {
|
||||
transformMatrix = sharings[sharing].transform;
|
||||
} else {
|
||||
transformMatrix = mat4(1.0);
|
||||
}
|
||||
|
||||
Vertex vertexIn;
|
||||
vec4 colorMesh;
|
||||
|
||||
if (mesh != -1) {
|
||||
vertexIn = meshVertices[mesh + offset];
|
||||
colorMesh = unpackUnorm4x8 (vertexIn.color);
|
||||
} else {
|
||||
vertexIn = verticesIn[indexIn];
|
||||
colorMesh = vec4 (1.0);
|
||||
}
|
||||
|
||||
vec4 colorIn = unpackUnorm4x8 (verticesIn[reference].color);
|
||||
|
||||
vec4 posOut = transformMatrix * vec4 (vertexIn.x, vertexIn.y, vertexIn.z, 1.0);
|
||||
vec4 colorOut = colorMesh * colorIn;
|
||||
|
||||
verticesOut[vertexOut].x = posOut.x;
|
||||
verticesOut[vertexOut].y = posOut.y;
|
||||
verticesOut[vertexOut].z = posOut.z;
|
||||
|
||||
verticesOut[vertexOut].u0 = vertexIn.u0;
|
||||
verticesOut[vertexOut].v0 = vertexIn.v0;
|
||||
|
||||
verticesOut[vertexOut].color = packUnorm4x8 (colorOut);
|
||||
|
||||
varyingsOut[varyingOut].offset = offset;
|
||||
varyingsOut[varyingOut].shouldCull = varyingsIn[reference].shouldCull;
|
||||
}
|
||||
)";
|
||||
|
||||
// Vanilla position_tex_color, spelled the way MC ships it: #version 150, no explicit
|
||||
// attribute locations (they come from glBindAttribLocation in format order) and the
|
||||
// ProjMat/ModelViewMat pair the mod re-uploads through setDefaultUniforms.
|
||||
constexpr const char* kBlitVertexSource = R"(#version 150
|
||||
in vec3 Position;
|
||||
in vec2 UV0;
|
||||
in vec4 Color;
|
||||
uniform mat4 ModelViewMat;
|
||||
uniform mat4 ProjMat;
|
||||
out vec2 texCoord0;
|
||||
out vec4 vertexColor;
|
||||
void main() {
|
||||
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
|
||||
texCoord0 = UV0;
|
||||
vertexColor = Color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kBlitFragmentSource = R"(#version 150
|
||||
uniform sampler2D Sampler0;
|
||||
in vec2 texCoord0;
|
||||
in vec4 vertexColor;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(Sampler0, texCoord0) * vertexColor;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kFboSize = 64;
|
||||
constexpr int kShaderStorageRestoreRange = 9;
|
||||
constexpr int kAtomicCounterRestoreRange = 1;
|
||||
constexpr int kBuilders = 2;
|
||||
|
||||
struct GuiVertex {
|
||||
float x, y, z;
|
||||
float u, v;
|
||||
std::uint32_t color;
|
||||
};
|
||||
static_assert(sizeof(GuiVertex) == 24, "POSITION_TEX_COLOR is 24 bytes");
|
||||
|
||||
struct VaryingData {
|
||||
std::int32_t offset;
|
||||
std::int32_t sharing;
|
||||
std::int32_t mesh;
|
||||
std::int32_t shouldCull;
|
||||
};
|
||||
|
||||
struct IndexedBinding {
|
||||
GLint buffer = 0;
|
||||
GLint start = 0;
|
||||
GLint size = 0;
|
||||
};
|
||||
|
||||
// Which parts of the mod's real frame this replay reproduces. Each test flips exactly
|
||||
// one on top of the baseline so a failure names the element that killed the quad.
|
||||
struct Fidelity {
|
||||
bool shortIndices = false; // MC's AutoStorageIndexBuffer is USHORT at small counts
|
||||
bool baseVertex = false; // ...and the second builder draws at a base vertex
|
||||
bool twoBuilders = false; // two render types share one output buffer
|
||||
bool blendAndDepth = false; // TRANSLUCENT_TRANSPARENCY + LEQUAL_DEPTH_TEST
|
||||
bool regrow = false; // MutableBuffer.doExpand replaces the GL name
|
||||
bool rewriteMapEachFrame = false;
|
||||
bool skipRelayout = false; // bindDrawBuffers() only re-lays-out when resized
|
||||
bool leaveMeshesUnbound = false; // a batch with no server meshes never binds binding 5
|
||||
};
|
||||
|
||||
class GuiBatchScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_transform = CompileComputeProgram(kTransformComputeSource);
|
||||
ASSERT_NE(m_transform, 0u) << m_buildLog;
|
||||
m_blit = CompileBlitProgram();
|
||||
ASSERT_NE(m_blit, 0u) << m_buildLog;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u);
|
||||
|
||||
MakeTexture();
|
||||
MakeIndexBuffers();
|
||||
MakeAcceleratedBuffers();
|
||||
m_laidOut = false;
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
DestroyColorFbo(m_target);
|
||||
if (m_transform) glDeleteProgram(m_transform);
|
||||
if (m_blit) glDeleteProgram(m_blit);
|
||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
||||
if (!m_buffers.empty()) glDeleteBuffers((GLsizei)m_buffers.size(), m_buffers.data());
|
||||
if (!m_vaos.empty()) glDeleteVertexArrays((GLsizei)m_vaos.size(), m_vaos.data());
|
||||
m_buffers.clear();
|
||||
m_vaos.clear();
|
||||
}
|
||||
|
||||
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[4096] = {};
|
||||
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[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileOne(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 == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// glBindAttribLocation in format order, exactly as ShaderInstance does it.
|
||||
unsigned int CompileBlitProgram() {
|
||||
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kBlitVertexSource);
|
||||
if (!vs) return 0;
|
||||
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kBlitFragmentSource);
|
||||
if (!fs) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glBindAttribLocation(program, 0, "Position");
|
||||
glBindAttribLocation(program, 1, "UV0");
|
||||
glBindAttribLocation(program, 2, "Color");
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("blit program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
void MakeTexture() {
|
||||
std::vector<std::uint8_t> pixels(4 * 4 * 4, 0);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
pixels[i * 4 + 2] = 255;
|
||||
pixels[i * 4 + 3] = 255;
|
||||
}
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
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_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
}
|
||||
|
||||
GLuint NewBuffer() {
|
||||
GLuint b = 0;
|
||||
glCreateBuffers(1, &b);
|
||||
m_buffers.push_back(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
GLuint NewVao() {
|
||||
GLuint v = 0;
|
||||
glCreateVertexArrays(1, &v);
|
||||
m_vaos.push_back(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
void MakeIndexBuffers() {
|
||||
std::uint32_t wide[12];
|
||||
std::uint16_t narrow[12];
|
||||
for (int quad = 0; quad < 2; ++quad) {
|
||||
const std::uint32_t base = (std::uint32_t)(quad * 4);
|
||||
const std::uint32_t pattern[6] = {base, base + 1, base + 2, base + 2, base + 3, base};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
wide[quad * 6 + i] = pattern[i];
|
||||
narrow[quad * 6 + i] = (std::uint16_t)pattern[i];
|
||||
}
|
||||
}
|
||||
m_wideIndices = NewBuffer();
|
||||
glNamedBufferStorage(m_wideIndices, sizeof(wide), wide, 0);
|
||||
m_narrowIndices = NewBuffer();
|
||||
glNamedBufferStorage(m_narrowIndices, sizeof(narrow), narrow, 0);
|
||||
}
|
||||
|
||||
static void SetupAttributes() {
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (const void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 24, (const void*)12);
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 24, (const void*)20);
|
||||
glEnableVertexAttribArray(2);
|
||||
}
|
||||
|
||||
void WriteInputs() {
|
||||
const GuiVertex unit[4] = {
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFFFFu},
|
||||
{0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0xFFFFFFFFu},
|
||||
{1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0xFFFFFFFFu},
|
||||
{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0xFFFFFFFFu},
|
||||
};
|
||||
for (int b = 0; b < kBuilders; ++b) {
|
||||
std::memcpy(m_inVertexMap[b], unit, sizeof(unit));
|
||||
VaryingData varyings[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
varyings[i].offset = i;
|
||||
varyings[i].sharing = b;
|
||||
varyings[i].mesh = -1;
|
||||
varyings[i].shouldCull = 0;
|
||||
}
|
||||
std::memcpy(m_inVaryingMap[b], varyings, sizeof(varyings));
|
||||
}
|
||||
}
|
||||
|
||||
void MakeAcceleratedBuffers() {
|
||||
const GLbitfield persistent = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
|
||||
for (int b = 0; b < kBuilders; ++b) {
|
||||
m_inVertices[b] = NewBuffer();
|
||||
glNamedBufferStorage(m_inVertices[b], 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, persistent);
|
||||
m_inVertexMap[b] =
|
||||
glMapNamedBufferRange(m_inVertices[b], 0, 4 * (GLsizeiptr)sizeof(GuiVertex), persistent);
|
||||
m_inVaryings[b] = NewBuffer();
|
||||
glNamedBufferStorage(m_inVaryings[b], 4 * (GLsizeiptr)sizeof(VaryingData), nullptr, persistent);
|
||||
m_inVaryingMap[b] =
|
||||
glMapNamedBufferRange(m_inVaryings[b], 0, 4 * (GLsizeiptr)sizeof(VaryingData), persistent);
|
||||
}
|
||||
|
||||
// Two SharingData entries: builder 0 lands left of centre, builder 1 right.
|
||||
float sharing[56] = {};
|
||||
const float tx[2] = {-0.9f, 0.1f};
|
||||
for (int b = 0; b < 2; ++b) {
|
||||
float* m = sharing + b * 28;
|
||||
m[0] = 0.8f;
|
||||
m[5] = 1.0f;
|
||||
m[10] = 1.0f;
|
||||
m[15] = 1.0f;
|
||||
m[12] = tx[b];
|
||||
m[13] = -0.5f;
|
||||
m[16] = 1.0f;
|
||||
m[20] = 1.0f;
|
||||
m[24] = 1.0f;
|
||||
}
|
||||
m_sharings = NewBuffer();
|
||||
glNamedBufferStorage(m_sharings, sizeof(sharing), nullptr, persistent);
|
||||
void* r = glMapNamedBufferRange(m_sharings, 0, sizeof(sharing), persistent);
|
||||
std::memcpy(r, sharing, sizeof(sharing));
|
||||
|
||||
m_outSize = 8 * (GLsizeiptr)sizeof(GuiVertex);
|
||||
m_outVertices = NewBuffer();
|
||||
glNamedBufferStorage(m_outVertices, m_outSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
m_outVaryings = NewBuffer();
|
||||
glNamedBufferStorage(m_outVaryings, 8 * (GLsizeiptr)sizeof(VaryingData), nullptr,
|
||||
GL_DYNAMIC_STORAGE_BIT);
|
||||
m_meshes = NewBuffer();
|
||||
glNamedBufferStorage(m_meshes, 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
|
||||
m_vao = NewVao();
|
||||
WriteInputs();
|
||||
}
|
||||
|
||||
std::vector<IndexedBinding> Record(GLenum b, GLenum s, GLenum z, int range) {
|
||||
std::vector<IndexedBinding> saved((std::size_t)range);
|
||||
for (int i = 0; i < range; ++i) {
|
||||
glGetIntegeri_v(b, (GLuint)i, &saved[(std::size_t)i].buffer);
|
||||
glGetIntegeri_v(s, (GLuint)i, &saved[(std::size_t)i].start);
|
||||
glGetIntegeri_v(z, (GLuint)i, &saved[(std::size_t)i].size);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
void Restore(GLenum target, const std::vector<IndexedBinding>& saved) {
|
||||
for (std::size_t i = 0; i < saved.size(); ++i) {
|
||||
if (saved[i].start == 0 && saved[i].size == 0) {
|
||||
glBindBufferBase(target, (GLuint)i, (GLuint)saved[i].buffer);
|
||||
} else {
|
||||
glBindBufferRange(target, (GLuint)i, (GLuint)saved[i].buffer, saved[i].start,
|
||||
saved[i].size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MutableBuffer.doExpand: a NEW immutable store, copied from the old one, old one gone.
|
||||
void RegrowOutputBuffer() {
|
||||
const GLsizeiptr newSize = m_outSize * 2;
|
||||
GLuint grown = 0;
|
||||
glCreateBuffers(1, &grown);
|
||||
glNamedBufferStorage(grown, newSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
glCopyNamedBufferSubData(m_outVertices, grown, 0, 0, m_outSize);
|
||||
glDeleteBuffers(1, &m_outVertices);
|
||||
for (auto& b : m_buffers) {
|
||||
if (b == m_outVertices) b = grown;
|
||||
}
|
||||
m_outVertices = grown;
|
||||
m_outSize = newSize;
|
||||
}
|
||||
|
||||
void Frame(const Fidelity& f, int builders) {
|
||||
if (f.rewriteMapEachFrame) WriteInputs();
|
||||
|
||||
// --- prepareBuffers() -------------------------------------------------
|
||||
const std::vector<IndexedBinding> ssbo =
|
||||
Record(GL_SHADER_STORAGE_BUFFER_BINDING, GL_SHADER_STORAGE_BUFFER_START,
|
||||
GL_SHADER_STORAGE_BUFFER_SIZE, kShaderStorageRestoreRange);
|
||||
const std::vector<IndexedBinding> counters =
|
||||
Record(GL_ATOMIC_COUNTER_BUFFER_BINDING, GL_ATOMIC_COUNTER_BUFFER_START,
|
||||
GL_ATOMIC_COUNTER_BUFFER_SIZE, kAtomicCounterRestoreRange);
|
||||
GLint currentProgram = 0;
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outVertices);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_sharings);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_outVaryings);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, f.leaveMeshesUnbound ? 0 : m_meshes);
|
||||
glUseProgram(m_transform);
|
||||
for (int b = 0; b < builders; ++b) {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inVertices[b]);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_inVaryings[b]);
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexCount"), 4u);
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexOffset"),
|
||||
(GLuint)(4 * b));
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "varyingOffset"),
|
||||
(GLuint)(4 * b));
|
||||
glDispatchCompute(1, 1, 1);
|
||||
}
|
||||
glUseProgram(0);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glUseProgram((GLuint)currentProgram);
|
||||
Restore(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
Restore(GL_ATOMIC_COUNTER_BUFFER, counters);
|
||||
|
||||
if (f.regrow) {
|
||||
RegrowOutputBuffer();
|
||||
m_laidOut = false; // isResized() forces the relayout
|
||||
}
|
||||
|
||||
// --- drawBuffers() ----------------------------------------------------
|
||||
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT |
|
||||
GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!m_laidOut || !f.skipRelayout) {
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_outVertices);
|
||||
SetupAttributes();
|
||||
m_laidOut = true;
|
||||
}
|
||||
|
||||
if (f.blendAndDepth) {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
}
|
||||
|
||||
const GLenum indexType = f.shortIndices ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT;
|
||||
const GLuint indexBuffer = f.shortIndices ? m_narrowIndices : m_wideIndices;
|
||||
const GLsizei indexStride = f.shortIndices ? 2 : 4;
|
||||
|
||||
for (int b = 0; b < builders; ++b) {
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); // AutoStorageIndexBuffer.bind
|
||||
glUseProgram(m_blit);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glUniform1i(glGetUniformLocation(m_blit, "Sampler0"), 0);
|
||||
UploadIdentityMatrices();
|
||||
if (f.baseVertex) {
|
||||
// The mod's BASEVERTEX path: every builder reads the SAME first six
|
||||
// indices and offsets the vertices with a base vertex.
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 6, indexType, (const void*)0, 4 * b);
|
||||
} else {
|
||||
glDrawElements(GL_TRIANGLES, 6, indexType, (const void*)(intptr_t)(b * 6 * indexStride));
|
||||
}
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
if (f.blendAndDepth) {
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void UploadIdentityMatrices() {
|
||||
static const float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ModelViewMat"), 1, GL_FALSE, identity);
|
||||
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ProjMat"), 1, GL_FALSE, identity);
|
||||
}
|
||||
|
||||
void ExpectQuads(const Image& image, int builders, const char* when) {
|
||||
EXPECT_TRUE(RegionIsMostly(image, 6, 26, 19, 45, "blue", 0.0,
|
||||
std::string("left accelerated quad, ") + when));
|
||||
if (builders > 1) {
|
||||
EXPECT_TRUE(RegionIsMostly(image, 38, 58, 19, 45, "blue", 0.0,
|
||||
std::string("right accelerated quad, ") + when));
|
||||
}
|
||||
}
|
||||
|
||||
void RunFrames(const Fidelity& f, int builders, int frames, const char* when) {
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Frame(f, builders);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "frame " << i << " raised a GL error (" << when << ")";
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ExpectQuads(image, builders, (std::string(when) + ", frame " + std::to_string(i)).c_str());
|
||||
Gl().EndFrame();
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int m_transform = 0;
|
||||
unsigned int m_blit = 0;
|
||||
ColorFbo m_target{};
|
||||
GLuint m_texture = 0;
|
||||
GLuint m_wideIndices = 0;
|
||||
GLuint m_narrowIndices = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_inVertices[kBuilders] = {};
|
||||
GLuint m_inVaryings[kBuilders] = {};
|
||||
void* m_inVertexMap[kBuilders] = {};
|
||||
void* m_inVaryingMap[kBuilders] = {};
|
||||
GLuint m_sharings = 0;
|
||||
GLuint m_outVertices = 0;
|
||||
GLuint m_outVaryings = 0;
|
||||
GLuint m_meshes = 0;
|
||||
GLsizeiptr m_outSize = 0;
|
||||
bool m_laidOut = false;
|
||||
std::vector<GLuint> m_buffers;
|
||||
std::vector<GLuint> m_vaos;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(GuiBatchScenario, Baseline) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunFrames(Fidelity{}, 1, 1, "baseline");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, ShortIndices) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.shortIndices = true;
|
||||
RunFrames(f, 1, 1, "short indices");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersWideIndices) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunFrames(Fidelity{}, 2, 1, "two builders, wide indices");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersBaseVertex) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
RunFrames(f, 2, 1, "two builders, base vertex");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersShortIndicesBaseVertex) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
RunFrames(f, 2, 1, "two builders, short indices, base vertex");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, BlendAndDepth) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
RunFrames(f, 2, 1, "blend and depth");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, ThreeFramesWithoutRelayout) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.skipRelayout = true;
|
||||
f.rewriteMapEachFrame = true;
|
||||
RunFrames(f, 2, 3, "three frames without relayout");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, MeshesBlockLeftUnbound) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.leaveMeshesUnbound = true;
|
||||
RunFrames(f, 2, 1, "Meshes block left unbound");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, FullFidelityWithRegrowth) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.skipRelayout = true;
|
||||
f.rewriteMapEachFrame = true;
|
||||
RunFrames(f, 2, 1, "full fidelity, pre-growth");
|
||||
f.regrow = true;
|
||||
RunFrames(f, 2, 1, "full fidelity, growth frame");
|
||||
f.regrow = false;
|
||||
RunFrames(f, 2, 2, "full fidelity, post-growth");
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Atomic counter blocks resolve through the SAME descriptor path as shader storage
|
||||
// blocks (glslang rewrites every atomic_uint into a synthesized storage block), so an
|
||||
// unbound GL_ATOMIC_COUNTER_BUFFER point loses the dispatch for exactly the same reason
|
||||
// an unbound SSBO did. The mod reaches this with its INDIRECT draw method, whose culling
|
||||
// shaders carry a counter the BASEVERTEX default never binds.
|
||||
//
|
||||
// The counter is INCREMENTED, not merely declared: an unreferenced one is optimised out
|
||||
// before it ever reaches a descriptor, so a shader that only declares it proves nothing.
|
||||
constexpr const char* kCounterComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint g_unbound;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
void main() {
|
||||
atomicCounterIncrement(g_unbound);
|
||||
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundCounterBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
GLint counters = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
||||
if (counters < 1) {
|
||||
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
|
||||
}
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &kCounterComputeSource, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
ASSERT_NE(compiled, GL_FALSE);
|
||||
m_program = glCreateProgram();
|
||||
glAttachShader(m_program, shader);
|
||||
glLinkProgram(m_program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_NE(linked, GL_FALSE);
|
||||
glGenBuffers(1, &m_buffer);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_buffer) glDeleteBuffers(1, &m_buffer);
|
||||
if (m_program) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
GLuint m_buffer = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(UnboundCounterBlockScenario, ADeclaredButUnboundCounterDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kElements = 4;
|
||||
const std::vector<unsigned int> zeros((std::size_t)kElements, 0u);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
|
||||
// Nothing is bound at GL_ATOMIC_COUNTER_BUFFER point 0 on purpose.
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(kElements, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
|
||||
|
||||
std::vector<unsigned int> values((std::size_t)kElements, 0xDEADBEEFu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(values.size() * sizeof(unsigned int)),
|
||||
values.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
EXPECT_EQ(values[(std::size_t)i], (unsigned int)(i + 1))
|
||||
<< "element " << i << " came back as " << values[(std::size_t)i]
|
||||
<< "; zero everywhere means the whole dispatch was dropped over the unbound counter block";
|
||||
}
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,391 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.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 - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER.
|
||||
//
|
||||
// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border
|
||||
// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and
|
||||
// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754
|
||||
// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on
|
||||
// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK.
|
||||
//
|
||||
// Two independent halves, and this scenario covers both because it goes through the frontend:
|
||||
//
|
||||
// * the STATE had no record of which entry point wrote the border colour. All three
|
||||
// representations are kept numerically in step, so the value alone cannot say whether the
|
||||
// application called glTexParameterfv or glTexParameterIiv.
|
||||
// * each backend then had exactly one border-colour call site: glTexParameterfv /
|
||||
// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on
|
||||
// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all.
|
||||
//
|
||||
// The border value is deliberately outside every predefined VkBorderColor and outside anything a
|
||||
// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black,
|
||||
// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color.
|
||||
// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit.
|
||||
//
|
||||
// Both an integer image view and an integer border colour are involved, which is the other half of
|
||||
// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour
|
||||
// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK.
|
||||
// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#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 kOutputWidth = 8;
|
||||
constexpr int kOutputHeight = 8;
|
||||
|
||||
// The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border
|
||||
// is deliberately not derivable from the texel.
|
||||
constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44};
|
||||
constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3};
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// One channel per draw, so a failure names the component that is wrong. The coordinate is a
|
||||
// uniform rather than a literal so the same program serves the border sample and the inside
|
||||
// sample and nothing can be constant-folded differently between them.
|
||||
std::string FragmentSource(int channel) {
|
||||
static const char* kChannels[4] = {"x", "y", "z", "w"};
|
||||
return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n"
|
||||
"out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") +
|
||||
kChannels[channel] + ";\n}\n";
|
||||
}
|
||||
|
||||
class IntegerBorderColorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
// 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory.
|
||||
const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}};
|
||||
glGenTextures(1, &m_sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
|
||||
|
||||
// 8x8 R32I render target: an integer readback, so nothing is normalized on the way
|
||||
// out and a wrong value is reported as the number it actually was.
|
||||
glGenTextures(1, &m_outputTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_sampler != 0) {
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &m_sampler);
|
||||
m_sampler = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
|
||||
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
|
||||
if (m_narrowTexture != 0) glDeleteTextures(1, &m_narrowTexture);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// Samples `coord` through the integer sampler and returns every texel the draw wrote.
|
||||
std::vector<std::int32_t> RenderChannel(int channel, float coordX, float coordY) {
|
||||
const std::string fragment = FragmentSource(channel);
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "channel " << channel << ": program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
// A clear value nothing under test can produce, so an undrawn target is not mistaken
|
||||
// for a correct one.
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, int channel, std::int32_t expected,
|
||||
const std::vector<std::int32_t>& texels) {
|
||||
if (texels.empty()) return;
|
||||
std::size_t offenders = 0;
|
||||
std::int32_t firstBad = 0;
|
||||
for (const std::int32_t texel : texels) {
|
||||
if (texel == expected) continue;
|
||||
if (offenders == 0) firstBad = texel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad
|
||||
<< " instead of " << expected << " (" << offenders << " of " << texels.size()
|
||||
<< " texels wrong)";
|
||||
}
|
||||
|
||||
// Every component of the border, in one place, so both the texture-object and the
|
||||
// sampler-object case assert exactly the same thing.
|
||||
void ExpectBorderIsDelivered(const char* what) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
// (-0.5, -0.5) is a full texture width outside the image on both axes, so
|
||||
// CLAMP_TO_BORDER can only answer with the border colour.
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, -0.5f, -0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind";
|
||||
ExpectAllTexels(what, channel, kBorderColor[channel], texels);
|
||||
}
|
||||
}
|
||||
|
||||
// A narrow-format source built on demand, for the clamp cases. Returns the texture, which
|
||||
// the caller owns until TearDown deletes it through m_narrowTexture.
|
||||
void MakeNarrowSource(GLenum internalFormat, GLenum clientFormat, const void* texels,
|
||||
const GLint* border, bool borderIsUnsigned) {
|
||||
glGenTextures(1, &m_narrowTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, clientFormat,
|
||||
internalFormat == GL_R8UI ? GL_UNSIGNED_BYTE : GL_BYTE, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
if (borderIsUnsigned) {
|
||||
const GLuint asUnsigned[4] = {static_cast<GLuint>(border[0]), static_cast<GLuint>(border[1]),
|
||||
static_cast<GLuint>(border[2]), static_cast<GLuint>(border[3])};
|
||||
glTexParameterIuiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, asUnsigned);
|
||||
} else {
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "narrow source setup left a GL error behind";
|
||||
}
|
||||
|
||||
// The narrow sources are single-channel, so only component 0 carries anything, and the
|
||||
// sampler declaration has to match the format's signedness.
|
||||
std::vector<std::int32_t> RenderNarrowBorder(bool isUnsignedSampler) {
|
||||
const std::string fragment =
|
||||
std::string("#version 330 core\n\nuniform ") + (isUnsignedSampler ? "usampler2D" : "isampler2D") +
|
||||
" smp;\nuniform vec2 uCoord;\n\nout int out_color;\n\nvoid main()\n{\n"
|
||||
" out_color = int(texture(smp, uCoord).x);\n}\n";
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "narrow-border program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), -0.5f, -0.5f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
GLuint m_sourceTexture = 0;
|
||||
GLuint m_outputTexture = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_sampler = 0;
|
||||
GLuint m_narrowTexture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE
|
||||
// sample has to fetch the texture's own texel. If this fails the sampler, the shader or the
|
||||
// integer readback is broken and nothing about the border colour has been measured.
|
||||
TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, 0.5f, 0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind";
|
||||
ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels);
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend
|
||||
// already accepted and then flattened into the same FloatVec4 every other spelling wrote.
|
||||
TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glTexParameterIiv");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, sampler-object spelling - which is the one the conformance cases actually use,
|
||||
// and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the
|
||||
// sampler cache key on DirectVulkan, where a border colour that is not part of the key would
|
||||
// alias two samplers that differ only in it).
|
||||
TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glGenSamplers(1, &m_sampler);
|
||||
ASSERT_NE(m_sampler, 0u);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected";
|
||||
|
||||
// The texture object carries a DIFFERENT border colour, so a pass here cannot come from the
|
||||
// texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state
|
||||
// wins over the texture's for every sampling parameter.
|
||||
const std::int32_t decoyBorder[4] = {0, 0, 0, 0};
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder);
|
||||
glBindSampler(0, m_sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glSamplerParameterIiv");
|
||||
glBindSampler(0, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "For floating-point and integer formats, border values are clamped to the
|
||||
// representable range of the format." A border of 300 on a GL_R8I texture is 127, not 300 - and
|
||||
// VK_BORDER_COLOR_INT_CUSTOM_EXT delivers whatever it is handed, with format VK_FORMAT_UNDEFINED
|
||||
// there is nothing for the driver to clamp against, so the clamp has to happen before the value
|
||||
// leaves MobileGL. DirectGLES gets it right for free (the ES driver knows the texture format),
|
||||
// which is what makes this a cross-backend divergence and not only a spec one.
|
||||
TEST_F(IntegerBorderColorScenario, ASignedIntegerBorderIsClampedToTheFormatsRepresentableRange) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::int8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {300, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8I, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/false);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8I border 300", 0, 127, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The reciprocal half, and the one that decides how the two integer forms relate: -1 written
|
||||
// through glTexParameterIiv against an UNSIGNED format. GL 4.6 core 8.10 stores an "I"-form
|
||||
// border unmodified with an integer internal data type and defines no sign conversion between
|
||||
// the two integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness: 0xFFFFFFFF, clamped to the format's maximum of 255.
|
||||
//
|
||||
// That is the DRIVER's answer, established by running this case rather than by reading the spec:
|
||||
// clamping to 0 is an equally defensible reading of the same paragraph, and DirectVulkan can be
|
||||
// made to produce either - but DirectGLES forwards the value to the ES driver verbatim and cannot
|
||||
// deviate, so choosing 0 would mean the same program sampling 0 on Magma and 255 on Espryt. The
|
||||
// whole point of carrying the border colour's form is to stop that class of divergence, so the
|
||||
// backends agree on the driver's answer.
|
||||
//
|
||||
// The clamp itself is still doing the work: without it the value reaches the driver as
|
||||
// 0xFFFFFFFF against a format whose maximum is 255, with format VK_FORMAT_UNDEFINED and so
|
||||
// nothing for the driver to clamp against.
|
||||
TEST_F(IntegerBorderColorScenario, ANegativeBorderOnAnUnsignedFormatClampsToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {-1, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border -1", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same clamp from the unambiguous side: a value written through the UNSIGNED form that is
|
||||
// simply too large for the format. No sign reinterpretation is involved, so both backends and
|
||||
// the spec agree that 5000 on a GL_R8UI texture is 255.
|
||||
TEST_F(IntegerBorderColorScenario, AnOversizedUnsignedBorderIsClampedToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {5000, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/true);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border 5000", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -137,7 +137,7 @@ namespace MGITest {
|
||||
// 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
|
||||
// the pack itself replays through the MagmaFixIterationRPSubgroupScratch 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 {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.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 - MESH-ARENA-SIZED BUFFERS, END TO END.
|
||||
//
|
||||
// A buffer store of at least 16MiB is adopted into the backend's persistently and
|
||||
// coherently mapped GPU storage the moment it is defined (BufferObject::
|
||||
// TryAdoptLargeStorage): the CPU shadow is dropped and every later write lands
|
||||
// directly in GPU-visible memory with no per-write driver call. Minecraft 26.3
|
||||
// streams chunk meshes into 128MB vertex arenas with plain glNamedBufferSubData -
|
||||
// on Mali, every driver-mediated route for that write into a busy mutable store
|
||||
// either parks the calling thread or ghost-copies the whole arena on a driver
|
||||
// worker (~167ms per touched arena: the recurring in-world hiccup this adoption
|
||||
// removed). Every existing buffer scenario uses stores far below the threshold,
|
||||
// so without this file the adopted path would have zero coverage.
|
||||
//
|
||||
// What is pinned, deliberately through the same API mix Minecraft uses:
|
||||
// * a glBufferSubData written AFTER the arena was drawn (in flight) reaches the
|
||||
// next draw - the write-visibility contract adoption must not weaken;
|
||||
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
|
||||
// * a compute-shader write through an SSBO binding of the same arena is read
|
||||
// back - the GPU-written path for adopted stores (glFinish + direct read).
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Comfortably past the 16MiB adoption threshold, and the vertex payload sits
|
||||
// deep inside the store so an implementation that quietly clamped or aliased
|
||||
// the adopted range would miss it.
|
||||
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(24) * 1024 * 1024;
|
||||
constexpr GLintptr kVertexOffset = GLintptr(20) * 1024 * 1024;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec3 a_color;
|
||||
out vec3 v_color;
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||
in vec3 v_color;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(v_color, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kMarkerComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Arena { uint word; };
|
||||
void main() { word = 0xC0FFEEu; }
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
float r, g, b;
|
||||
};
|
||||
|
||||
// A full-viewport quad, colored uniformly so one center readback speaks for
|
||||
// the whole draw.
|
||||
std::vector<Vertex> QuadVertices(float r, float g, float b) {
|
||||
return {
|
||||
{-1.f, -1.f, r, g, b}, {1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b},
|
||||
{-1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b}, {-1.f, 1.f, r, g, b},
|
||||
};
|
||||
}
|
||||
|
||||
class LargeArenaAdoptionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = LinkProgram(kVertexSource, kFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_arena);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
// The NULL-data definition is the adoption point (and Minecraft's
|
||||
// arena-creation idiom).
|
||||
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<void*>(kVertexOffset));
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_compute != 0) glDeleteProgram(m_compute);
|
||||
m_vao = 0;
|
||||
m_arena = 0;
|
||||
m_program = 0;
|
||||
m_compute = 0;
|
||||
}
|
||||
|
||||
unsigned int 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 == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
unsigned int LinkProgram(const char* vs, const char* fs) {
|
||||
const GLuint v = CompileStage(GL_VERTEX_SHADER, vs);
|
||||
if (v == 0) return 0;
|
||||
const GLuint f = CompileStage(GL_FRAGMENT_SHADER, fs);
|
||||
if (f == 0) {
|
||||
glDeleteShader(v);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, v);
|
||||
glAttachShader(program, f);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(v);
|
||||
glDeleteShader(f);
|
||||
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("program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
void UploadQuad(float r, float g, float b) {
|
||||
const auto vertices = QuadVertices(r, g, b);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
}
|
||||
|
||||
void DrawQuad() {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
std::array<unsigned char, 4> CenterPixel() {
|
||||
std::array<unsigned char, 4> px = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
px.data());
|
||||
return px;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_compute = 0;
|
||||
unsigned int m_vao = 0;
|
||||
unsigned int m_arena = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The Minecraft shape: the arena is drawn, the frame retires, and a
|
||||
// glBufferSubData rewrites the SAME vertex bytes while the previous frame's
|
||||
// draw may still be in flight. The next draw must show the NEW bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, SubDataAfterAnInFlightDrawReachesTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
DrawQuad();
|
||||
auto px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[0], 200) << "the first draw from the adopted arena never landed";
|
||||
EXPECT_LT(px[1], 50);
|
||||
|
||||
Gl().EndFrame();
|
||||
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
DrawQuad();
|
||||
px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[1], 200) << "the cross-frame rewrite of the adopted arena did not reach the draw; "
|
||||
"the old color means the write went to bytes the draw no longer reads";
|
||||
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||
}
|
||||
|
||||
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
||||
// back exactly those bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const auto vertices = QuadVertices(0.25f, 0.5f, 0.75f);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
std::vector<Vertex> read(vertices.size());
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(read.size() * sizeof(Vertex)), read.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(0, std::memcmp(read.data(), vertices.data(), read.size() * sizeof(Vertex)))
|
||||
<< "GetBufferSubData of the adopted arena returned different bytes than the SubData wrote";
|
||||
}
|
||||
|
||||
// A GPU write through an SSBO binding of the adopted arena must be visible to
|
||||
// a CPU readback - the path that waits out the GPU and reads the coherent
|
||||
// mapping directly.
|
||||
TEST_F(LargeArenaAdoptionScenario, GpuWriteIntoTheArenaIsReadBack) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLint maxComputeStorageBlocks = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
|
||||
if (maxComputeStorageBlocks < 1) {
|
||||
GTEST_SKIP() << "no compute shader storage blocks on this driver";
|
||||
}
|
||||
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
|
||||
ASSERT_NE(compute, 0u) << m_buildLog;
|
||||
m_compute = glCreateProgram();
|
||||
glAttachShader(m_compute, compute);
|
||||
glLinkProgram(m_compute);
|
||||
glDeleteShader(compute);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_compute, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
|
||||
const unsigned int seed = 0u;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(seed), &seed);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
|
||||
glUseProgram(m_compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
unsigned int marker = 0;
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(marker, 0xC0FFEEu)
|
||||
<< "the compute write into the adopted arena did not reach the CPU readback";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
File diff suppressed because it is too large
Load Diff
@@ -518,5 +518,62 @@ void main() {
|
||||
ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws");
|
||||
}
|
||||
|
||||
// The base-vertex family's argument checks (GL 4.6 core 10.3.9). These are what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_* assert, and the reason the group sat
|
||||
// NotSupported for so long hid the fact that the entry points forwarded any argument
|
||||
// straight to the backend: a negative count reached the emulation as a huge unsigned
|
||||
// size. Each case drains the error queue first so the assertion names the call it made.
|
||||
TEST_F(MultiDrawScenario, BaseVertexDrawsRejectMalformedArguments) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 0;
|
||||
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||
// A bound program and VAO are prerequisites, not decoration: the entry points check
|
||||
// "is there something to execute" (GL_INVALID_OPERATION) before they look at any
|
||||
// argument, so without these every case below would pass for the wrong reason.
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
|
||||
const auto expectError = [&](const char* what, GLenum expected) {
|
||||
EXPECT_EQ(FirstGLError(), expected) << what;
|
||||
// FirstGLError stops at the first one; make sure nothing else is queued so the
|
||||
// next case starts clean.
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
};
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, -1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a negative count", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 3, GL_NONE, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a non-index type", GL_INVALID_ENUM);
|
||||
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, 3, 0, 3, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with end < start", GL_INVALID_VALUE);
|
||||
|
||||
// start = -1 arrives as 0xFFFFFFFF, so this is the same end < start rule seen from
|
||||
// the other side - and it is the shape the CTS's invalid_count case actually uses.
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, static_cast<GLuint>(-1), 2, 1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with a wrapped start", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsInstancedBaseVertex(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr, -1, 0);
|
||||
expectError("glDrawElementsInstancedBaseVertex with a negative instancecount", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei negativeCount = -1;
|
||||
const void* offsets[1] = {reinterpret_cast<const void*>(0)};
|
||||
const GLint baseVertices[1] = {0};
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &negativeCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative element of count", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei validCount = 6;
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, -1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative drawcount", GL_INVALID_VALUE);
|
||||
|
||||
// The well-formed call still has to go through, or the checks above would be
|
||||
// indistinguishable from a blanket rejection.
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("a well-formed glMultiDrawElementsBaseVertex", GL_NO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PointSizeDemotionScenario.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 - THE gl_PointSize DEMOTION IS CLIENT-INVISIBLE, AND IT ACTUALLY ARMS.
|
||||
//
|
||||
// On a device that hosts the built-in in tessellation/geometry stages (llvmpipe and
|
||||
// lavapipe both do), gl_PointSize travels as itself; on one that does not (the Mali
|
||||
// devices this exists for), phase B demotes it to an ordinary varying
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram) and the capture
|
||||
// machinery follows it there. This scenario runs in BOTH configurations and must hand
|
||||
// back identical bytes: the ambient registrations take the native path, and the
|
||||
// PointSizeDemotion. registrations pin MOBILEGL_POINT_SIZE_DEMOTION=1 so the demotion
|
||||
// runs on the same healthy drivers - CopyImagePacked16Scenario's dual-configuration
|
||||
// contract, applied to a value chain instead of a storage format.
|
||||
//
|
||||
// The VALUE is the whole contract: every case writes gl_PointSize in one stage, reads it
|
||||
// back out of gl_in[] in the next, and captures it by name under rasterizer discard, so
|
||||
// one wrong link anywhere in VS -> TCS -> TES -> GS -> capture lands in the readback.
|
||||
// The RASTERIZED size is deliberately not asserted anywhere: with the built-in unhosted
|
||||
// it falls back to 1.0 by spec on both targets, which is exactly the honest residue the
|
||||
// demotion documents (point_rendering-style bodies keep failing truthfully).
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on
|
||||
// the absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 460 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The full chain, with per-vertex VARIATION seeded in the vertex stage so a control
|
||||
// invocation that read or wrote the wrong slot changes the sum: 2,3,4 arrive, 3,4,5
|
||||
// leave, the evaluation stage sums its patch to 12, the geometry stage doubles what
|
||||
// it read to 24.
|
||||
const char* const kChainVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 2.0 + float(gl_VertexID);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessControlSource = R"(#version 460 core
|
||||
layout(vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessEvalSource = R"(#version 460 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// The geometry-only chain: no tessellation required of the stack at all.
|
||||
const char* const kPointVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 7.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// A capture stage that only READS the incoming point size and never writes its own.
|
||||
// Legal GL, and the shape that separates "the demotion arms" from "the demotion knows
|
||||
// a capture is coming": with the built-in gone, only the capture request can put a
|
||||
// carrier back for a by-name capture to bind to.
|
||||
const char* const kReadOnlyGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float g_echo;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
g_echo = gl_in[0].gl_PointSize;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kEchoFragmentSource = R"(#version 460 core
|
||||
in float g_echo;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(g_echo, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PointSizeDemotionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()),
|
||||
varyings.data(), GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over `vertexCount` vertices of `drawMode`, recorded as
|
||||
// GL_POINTS. The buffer is poison-filled first so bytes the capture never wrote
|
||||
// name themselves.
|
||||
std::vector<float> RunCaptureSpan(GLuint program, GLenum drawMode, GLsizei vertexCount,
|
||||
std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(drawMode, 0, vertexCount);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data,
|
||||
std::size_t index, float expected,
|
||||
float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture "
|
||||
<< "never reached these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// The library log, for the arming case. Same machinery and same reasoning as
|
||||
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, the file
|
||||
// is appended to by every process in the lane, and only bytes appended after the
|
||||
// snapshot may satisfy an assertion.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
|
||||
private:
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
};
|
||||
|
||||
// The five-stage chain. 24.0 can only arrive if the vertex mirror, both control-stage
|
||||
// redirects (read AND write), the evaluation stage's three gl_in reads and the
|
||||
// geometry stage's read all carried the right value - one wrong link and the sum
|
||||
// moves. point_mode with every level at 1 emits three points; the first record proves
|
||||
// the mechanism, exactly as TessellationXfbCaptureScenario reasons.
|
||||
TEST_F(PointSizeDemotionScenario, TheValueSurvivesTheFiveStageChainIntoTheCapture) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_GEOMETRY_SHADER, kChainGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 24.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same chain without a geometry stage: the capture then binds to the evaluation
|
||||
// stage's value (the sum, 12.0) - which is also the boundary where a demoted program
|
||||
// switches its capture carrier from the Io chain to the capture name.
|
||||
TEST_F(PointSizeDemotionScenario, TheEvaluationStageOwnsTheCaptureWithoutAGeometryStage) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 12.0f));
|
||||
|
||||
// The GL query surface keeps the truthful spelling whatever the backends renamed
|
||||
// underneath: reflection is a phase-A product and the demotion happens after it.
|
||||
char varyingName[64] = {};
|
||||
GLsizei nameLength = 0;
|
||||
GLsizei varyingSize = 0;
|
||||
GLenum varyingType = 0;
|
||||
glGetTransformFeedbackVarying(program, 0, sizeof(varyingName), &nameLength, &varyingSize,
|
||||
&varyingType, varyingName);
|
||||
EXPECT_STREQ(varyingName, "gl_PointSize");
|
||||
EXPECT_EQ(varyingType, static_cast<GLenum>(GL_FLOAT));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The geometry-only chain: gl_in[0].gl_PointSize read straight off the vertex stage,
|
||||
// no tessellation involved - the VS -> GS boundary of the demotion on its own.
|
||||
TEST_F(PointSizeDemotionScenario, AGeometryOnlyChainCarriesTheVertexValue) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE CAPTURE-REQUEST PATH, END TO END - the half no unit test can reach, because the
|
||||
// request travels from glTransformFeedbackVaryings through phase A's resolved capture
|
||||
// set and the phase-B handoff before it reaches the demotion.
|
||||
//
|
||||
// The geometry stage READS gl_in[0].gl_PointSize and never writes gl_PointSize, which
|
||||
// is enough to arm the demotion (glslang declares GeometryPointSize on a read) but not
|
||||
// enough to create an output carrier on its own. Only the capture request can, and if
|
||||
// that request never arrives the program does not merely lose the point-size column:
|
||||
// DirectGLES respells the driver-side capture to a name no stage declares and the
|
||||
// WHOLE capture set fails to link, while DirectVulkan mirrors a built-in the demotion
|
||||
// just removed and can unwind far enough to drop the Xfb execution mode. Either way
|
||||
// g_echo - an ordinary varying with nothing to do with point size - comes back poison,
|
||||
// which is what this asserts. gl_PointSize itself is captured but never asserted: no
|
||||
// stage writes it, so GL leaves its value undefined.
|
||||
TEST_F(PointSizeDemotionScenario, ACaptureSurvivesAStageThatOnlyReadsThePointSize) {
|
||||
if (!Ready()) return;
|
||||
// The NATIVE Espryt path cannot do this at all, and never could: with the built-in
|
||||
// hosted, the geometry stage's ESSL simply does not declare gl_PointSize unless it
|
||||
// writes it, so the driver rejects the capture request with "varying undeclared"
|
||||
// and the program becomes unusable. That is a pre-existing ES limitation the
|
||||
// demotion happens to REPAIR - the carrier is a real, seeded, declared varying -
|
||||
// so this case has something to assert only where the demotion is armed. Magma
|
||||
// consumes SPIR-V and answers on both paths, which keeps the negative control.
|
||||
if (Gl().BackendName() == "DirectGLES" &&
|
||||
AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "Espryt cannot capture a gl_PointSize its capture stage never "
|
||||
"writes without the demotion; the PointSizeDemotion. ctest entry "
|
||||
"runs this same case with MOBILEGL_POINT_SIZE_DEMOTION=1";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kReadOnlyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kEchoFragmentSource}},
|
||||
{"g_echo", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "the capture set failed to link. On a demoting configuration this is the "
|
||||
"capture request never reaching the demotion, so the point-size capture was "
|
||||
"respelled to a carrier no stage declares. Build log: "
|
||||
<< m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 2);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f))
|
||||
<< "the unrelated varying captured alongside gl_PointSize did not survive; the "
|
||||
"point-size capture took the whole set with it";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE DEMOTION SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above captures the right bytes on llvmpipe and lavapipe whether the
|
||||
// demotion ran or not - these machines host the built-in - so those cases pin that
|
||||
// the demotion does no HARM and can say nothing about whether it happened. The
|
||||
// arming is where the cheap mistake lives: MOBILEGL_POINT_SIZE_DEMOTION maps onto
|
||||
// the two Supports*PointSize capability bits INVERTED (forcing the demotion on
|
||||
// means declaring the built-in UNHOSTED), and a swap of those arms - or a dropped
|
||||
// env bit anywhere between ConfigLoader, the backend init, CompileEnv and the L1
|
||||
// key - would disable the device repair with every rendering case still green.
|
||||
//
|
||||
// Same machinery as UnlocatedIoBlockScenario's arming case: the environment says
|
||||
// the demotion is pinned on, therefore the library must SAY it demoted something.
|
||||
// The observable is the latched MGLOG_I each backend emits when it first builds a
|
||||
// demoted program; both spell "demoted to an ordinary varying", so this one case
|
||||
// covers both pinned lanes without a backend gate.
|
||||
TEST_F(PointSizeDemotionScenario, TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the demotion pinned ON for the whole process, which "
|
||||
"is what the PointSizeDemotion. ctest entries do with "
|
||||
"MOBILEGL_POINT_SIZE_DEMOTION=1; with the variable unset the detected "
|
||||
"capabilities decide, and on this machine the built-in is hosted - so "
|
||||
"there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_POINT_SIZE_DEMOTION is pinned on but MOBILEGL_LOG_FILE_PATH "
|
||||
"is not set, so the library has nowhere to record that it demoted "
|
||||
"anything; the PointSizeDemotion. ctest entries set both";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
// Drawn as well as built, so a stack that defers its backend program to first
|
||||
// use still reaches the build the latched line fires in - and the capture must
|
||||
// STILL be right through the carrier.
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f))
|
||||
<< "the pinned-on lane did not even capture correctly";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("demoted to an ordinary varying"), std::string::npos)
|
||||
<< "MOBILEGL_POINT_SIZE_DEMOTION is pinned ON, a geometry program reading and "
|
||||
"writing gl_PointSize was built and captured, and no backend ever reported "
|
||||
"demoting it. The demotion is not armed - check the override mapping in the "
|
||||
"backend inits (it is inverted on purpose), the CompileEnv accessors, and "
|
||||
"ProgramSpirvTask's verdict plumbing. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,404 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitiveRestartScenario.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 - DESKTOP GL_PRIMITIVE_RESTART WITH AN APPLICATION-CHOSEN INDEX.
|
||||
//
|
||||
// Desktop GL restarts on whatever glPrimitiveRestartIndex named; GLES and Vulkan both restart
|
||||
// only on the all-ones value of the index type. DirectGLES used to THROW_EXCEPTION on the
|
||||
// mismatch, and a throw out of a GL entry point unwinds a C++ exception through the C ABI and
|
||||
// kills the process - which is how KHR-GL4x.geometry_shader.primitive_counter.*_rp took the whole
|
||||
// conformance runner down, nine bodies at a time, losing every result in the chunk with it.
|
||||
//
|
||||
// So the first thing this asserts is simply that the process is still here. The second is that
|
||||
// the restart actually happened: the substitution rewrites the index data so the driver restarts
|
||||
// where the application asked, and the difference between "restart honoured" and "restart
|
||||
// silently dropped" is a triangle strip that welds its two halves together across the gap.
|
||||
//
|
||||
// Needs a real context on purpose. The GPU-free suite cannot reach a backend at all, and this is
|
||||
// entirely about what the backend does with the index buffer.
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSurface = 64;
|
||||
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in vec2 a_position;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Two triangles with a gap down the middle, plus two spare vertices parked at the origin.
|
||||
//
|
||||
// The spares exist so the restart index is a LEGAL vertex index: if the restart were
|
||||
// dropped the driver would still fetch a real vertex rather than read out of bounds, so
|
||||
// the negative case is defined behaviour and the test measures the restart rather than
|
||||
// whatever robust-buffer-access does.
|
||||
constexpr GLfloat kVertices[] = {
|
||||
-0.9f, -0.9f, // 0 - left triangle
|
||||
-0.1f, -0.9f, // 1
|
||||
-0.9f, 0.9f, // 2
|
||||
0.1f, -0.9f, // 3 - right triangle
|
||||
0.9f, -0.9f, // 4
|
||||
0.9f, 0.9f, // 5
|
||||
0.0f, 0.0f, // 6 - spare
|
||||
0.0f, 0.0f, // 7 - spare, and the application's restart index
|
||||
};
|
||||
constexpr GLuint kRestartIndex = 7;
|
||||
|
||||
// A triangle STRIP, restarted in the middle: honoured, it is exactly the two triangles
|
||||
// above. Dropped, the strip welds vertices 2, 7 and 3 into extra triangles that spill
|
||||
// across the gap - which is what the middle probe below catches.
|
||||
constexpr GLuint kIndices[] = {0, 1, 2, kRestartIndex, 3, 4, 5};
|
||||
|
||||
struct Pixel {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
};
|
||||
|
||||
class PrimitiveRestartScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glGenBuffers(1, &m_ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
|
||||
glGenTextures(1, &m_colorTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_colorTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSurface, kSurface);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
|
||||
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSurface, kSurface);
|
||||
|
||||
m_program = BuildProgram();
|
||||
ASSERT_NE(m_program, 0u) << "the flat-colour program did not build: " << m_buildLog;
|
||||
glUseProgram(m_program);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
glPrimitiveRestartIndex(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_colorTexture != 0) glDeleteTextures(1, &m_colorTexture);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// The whole surface, so a failure can report the three probes together rather than
|
||||
// three separate readbacks that might disagree about which draw they saw.
|
||||
std::vector<Pixel> DrawAndRead() {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(kIndices)), GL_UNSIGNED_INT,
|
||||
nullptr);
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
static const Pixel& At(const std::vector<Pixel>& pixels, int x, int y) {
|
||||
return pixels[static_cast<std::size_t>(y) * kSurface + x];
|
||||
}
|
||||
|
||||
static bool IsGreen(const Pixel& p) { return p.g > 128 && p.r < 128; }
|
||||
|
||||
// NDC (-0.5, -0.5): well inside the left triangle whichever way the restart went.
|
||||
static constexpr int kLeftX = 16, kLeftY = 16;
|
||||
// NDC (0.6, -0.5): well inside the right triangle, and outside every welded one.
|
||||
static constexpr int kRightX = 51, kRightY = 16;
|
||||
// NDC (0.2, -0.5): in the gap between the two triangles, and INSIDE the triangle the
|
||||
// strip welds out of vertices 7, 3 and 4 when the restart is dropped. This is the
|
||||
// probe that distinguishes a working restart from a silently ignored one.
|
||||
static constexpr int kGapX = 38, kGapY = 16;
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_colorTexture = 0;
|
||||
GLuint m_program = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// THE crash regression. Before the fix this call never returned: DirectGLES threw
|
||||
// std::runtime_error out of glDrawElements and the process died on the spot. Reaching the
|
||||
// assertion at all is most of the point.
|
||||
TEST_F(PrimitiveRestartScenario, AnArbitraryRestartIndexDrawsInsteadOfKillingTheProcess) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "an arbitrary restart index is legal desktop GL and must raise no error";
|
||||
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the gap between the two halves is covered, so the restart was dropped and the "
|
||||
"strip welded across it";
|
||||
}
|
||||
|
||||
// The other half of the state: an application that sets the restart index TO the fixed
|
||||
// all-ones value needs no rewriting at all, and the cap must map straight onto the
|
||||
// driver's own fixed-index restart. Same picture, different path through the backend.
|
||||
TEST_F(PrimitiveRestartScenario, TheFixedIndexValueTakesTheForwardingPath) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// Index 0xFFFFFFFF is not a vertex this draw uses, so the strip is the same shape.
|
||||
const GLuint fixedIndices[] = {0, 1, 2, 0xFFFFFFFFu, 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(fixedIndices), fixedIndices);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0xFFFFFFFFu);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY)));
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)));
|
||||
|
||||
// Put the buffer back for whatever runs next in this fixture.
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(kIndices), kIndices);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// With the cap off, the same index data is just data - nothing restarts, and the strip
|
||||
// welds across the gap. The negative control for the probe above: without it, a backend
|
||||
// that lost the whole draw would pass the test by rendering nothing in the gap.
|
||||
TEST_F(PrimitiveRestartScenario, WithoutTheCapTheStripWeldsAcrossTheGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the draw itself must still happen";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "with restart disabled the strip is continuous, so the gap must be covered - if "
|
||||
"it is not, the probe above proves nothing";
|
||||
}
|
||||
|
||||
// A second draw with a DIFFERENT restart index has to be rewritten again. The substitution
|
||||
// stages through one scratch buffer, so a cached or half-restored element-array binding
|
||||
// would show up here as the second draw reusing the first one's data.
|
||||
TEST_F(PrimitiveRestartScenario, ChangingTheRestartIndexBetweenDrawsIsHonoured) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
const std::vector<Pixel> restarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FALSE(IsGreen(At(restarted, kGapX, kGapY)));
|
||||
|
||||
// 6 is the other spare vertex, and it appears nowhere in the index data - so nothing
|
||||
// restarts and the strip is continuous again, from the very same buffer.
|
||||
glPrimitiveRestartIndex(6);
|
||||
const std::vector<Pixel> notRestarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kGapX, kGapY)))
|
||||
<< "the second draw restarted on an index that is not in its data";
|
||||
}
|
||||
|
||||
// A NON-indexed draw has no index stream, so GL primitive restart cannot affect it - and a
|
||||
// list topology is the shape DirectVulkan has to refuse when the device lacks
|
||||
// VK_EXT_primitive_topology_list_restart. Deriving the pipeline's primitiveRestartEnable
|
||||
// from the capability bits alone conflated the two: an application that enables
|
||||
// GL_PRIMITIVE_RESTART once at init and then draws its UI with glDrawArrays(GL_TRIANGLES)
|
||||
// had every one of those draws silently dropped on such a device.
|
||||
TEST_F(PrimitiveRestartScenario, ANonIndexedListTopologyDrawIsUnaffectedByTheCap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Vertices 0,1,2 are the left triangle; GL_TRIANGLES is a list topology.
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)))
|
||||
<< "primitive restart has no meaning for glDrawArrays, so the draw must render "
|
||||
"normally whatever the device supports";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.6 compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX. A restart index the index type cannot hold therefore matches
|
||||
// nothing and the draw restarts NOWHERE - it does not restart on the truncated value, and
|
||||
// it does not restart on the type's all-ones value either, which is what the driver's own
|
||||
// fixed-index restart would have done if it had been left enabled.
|
||||
TEST_F(PrimitiveRestartScenario, ARestartIndexTooLargeForTheIndexTypeRestartsNowhere) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// 16-bit indices with a restart index of 0x10007: the low half (7) IS a real index in
|
||||
// the data, so a truncating comparison would split the strip exactly where a correct
|
||||
// one leaves it whole.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0x10000u + kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(shortIndices)), GL_UNSIGNED_SHORT,
|
||||
nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "no 16-bit index can equal 0x10007, so nothing restarts and the strip is "
|
||||
"continuous - truncating the restart index to 7 would split it here";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The all-ones value of an index type is an ordinary vertex index whenever the array uses
|
||||
// the type's full range, which is exactly why an application picks an arbitrary restart
|
||||
// index in the first place. Substituting the sentinel in place would either steal that
|
||||
// vertex or spuriously restart on it, so the copy widens instead - and the draw has to be
|
||||
// issued with the widened type, which is the part that is easy to forget.
|
||||
TEST_F(PrimitiveRestartScenario, AnAllOnesVertexIndexSurvivesTheSubstitution) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// The buffer carries the 16-bit all-ones value as an ordinary element. It sits past
|
||||
// the seven indices this draw reads, because the vertex array has only eight entries
|
||||
// and fetching index 65535 would be out of range - what is under test is that its
|
||||
// mere PRESENCE forces the widened copy, and that the draw still finds its own
|
||||
// indices at the right offsets in a copy whose element width has changed underneath
|
||||
// it. Narrowly substituting in place instead would rewrite this element to 0xFFFE.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5, 0xFFFFu};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Only the first seven indices are drawn, so the 0xFFFF element is never fetched - what
|
||||
// is under test is that its PRESENCE does not break the substitution or the offsets.
|
||||
glDrawElements(GL_TRIANGLE_STRIP, 7, GL_UNSIGNED_SHORT, nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the restart still has to happen once the copy has been widened";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,554 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.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 - GL_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
|
||||
// INACTIVE.
|
||||
//
|
||||
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
|
||||
// capture or no capture. The CTS leans its whole tessellation suite on that - the
|
||||
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
|
||||
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
|
||||
// tessellation tests are sized from the answer - so a backend that answers 0
|
||||
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
|
||||
//
|
||||
// DirectVulkan serves the query from the transform-feedback stream query's
|
||||
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
|
||||
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
|
||||
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
|
||||
// the pair reads back 0. Where the bring-up probe measures that defect with a
|
||||
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
|
||||
// renderer accumulates XFB-inactive draws through the best proven substitute
|
||||
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
|
||||
// rasterizer discard included), else pipeline statistics over clipping-stage
|
||||
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
|
||||
// answer, so on this machine they hold the reroute to the same numbers the
|
||||
// healthy stream path must produce - the "two pools must agree" assertion - and
|
||||
// on a healthy driver they pin the stream path itself.
|
||||
//
|
||||
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
|
||||
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
|
||||
// is a different mechanism with its own tests.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
|
||||
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
|
||||
// same program and simply never begin a span.
|
||||
const char* const kVertexSource = R"(#version 430 core
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vs_out_value = vec4(1.0);
|
||||
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
|
||||
// triangle per patch - the count the tessellation cases assert.
|
||||
const char* const kTessVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* const kTessControlSource = R"(#version 430 core
|
||||
layout(vertices = 1) out;
|
||||
void main() {
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
const char* const kTessEvalSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
void main() {
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The same tessellation pipeline with something to capture, so that
|
||||
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
|
||||
// open (but paused) capture span AND a tessellator in one program.
|
||||
const char* const kTessEvalCaptureSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
out vec4 te_out_value;
|
||||
void main() {
|
||||
te_out_value = vec4(1.0);
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != std::string("DirectVulkan")) {
|
||||
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
|
||||
<< Gl().BackendName()
|
||||
<< " answers this query from a different mechanism";
|
||||
}
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenQueries(2, m_queries);
|
||||
ASSERT_NE(m_queries[0], 0u);
|
||||
ASSERT_NE(m_queries[1], 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
|
||||
m_queries[0] = m_queries[1] = 0;
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// captureVarying: the name to record with glTransformFeedbackVaryings, or
|
||||
// nullptr for a program that can never open a capture span.
|
||||
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||
const char* captureVarying) {
|
||||
std::vector<GLuint> shaders;
|
||||
for (const auto& [type, source] : stages) {
|
||||
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||
if (shader == 0) {
|
||||
for (const GLuint built : shaders) glDeleteShader(built);
|
||||
return 0;
|
||||
}
|
||||
shaders.push_back(shader);
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (captureVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram() {
|
||||
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
|
||||
}
|
||||
|
||||
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTessGenLevel < 1) return 0;
|
||||
return BuildProgram(
|
||||
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER,
|
||||
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
|
||||
withCaptureVarying ? "te_out_value" : nullptr);
|
||||
}
|
||||
|
||||
// A capture span that is open but PAUSED. The pause closes the capture, so
|
||||
// every draw inside it is XFB-inactive at the backend - the stream query's
|
||||
// silent case - while the GL span stays active. `program` must be the one
|
||||
// that is bound: GL requires the same program at resume.
|
||||
void BeginPausedSpan() {
|
||||
glGenBuffers(1, &m_captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glPauseTransformFeedback();
|
||||
}
|
||||
|
||||
void EndPausedSpan() {
|
||||
glResumeTransformFeedback();
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
|
||||
m_captureBuffer = 0;
|
||||
}
|
||||
|
||||
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||
GLuint QueryGenerated(const std::function<void()>& record) {
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
||||
record();
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint generated = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
static GLenum DrainGLErrors() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||
GLuint m_captureBuffer = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// The plain shape: no capture object was ever bound, no span begun, no
|
||||
// rasterizer discard - just a GENERATED query around two triangles. On a
|
||||
// healthy driver the stream query answers it; on an affected one the armed
|
||||
// reroute must produce the same 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
|
||||
"inactive (GL 4.6 core 13.4)";
|
||||
}
|
||||
|
||||
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
|
||||
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
|
||||
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
|
||||
// tests' capture buffers on the affected device.
|
||||
//
|
||||
// On lavapipe this case holds through the dedicated
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
|
||||
// what makes a discarded draw countable there - llvmpipe's clipping
|
||||
// statistics AND stream query both read 0 under discard).
|
||||
//
|
||||
// The value-conditioned skip below is deliberate and narrow, for a stack
|
||||
// with NO counter that survives discard: there this case is unfalsifiable,
|
||||
// and a red would indict MobileGL for a hole the bring-up probe already
|
||||
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
|
||||
// exact-zero answer IS the capability signal - any wrong nonzero count
|
||||
// still fails - and on every driver that counts discarded draws at all the
|
||||
// full assertion runs. The device probe list holds this shape on the Mali.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (generated == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
|
||||
"primitives-generated query, clipping statistics) survives "
|
||||
"rasterizer discard for an XFB-inactive draw on this stack - the "
|
||||
"shape is unfalsifiable here; the bring-up probe measures the same "
|
||||
"hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "rasterizer discard drops primitives after clipping and must not hide them from "
|
||||
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
|
||||
"tessellator with";
|
||||
}
|
||||
|
||||
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
|
||||
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
|
||||
// protocol derives everything from. Undiscarded, so that the answer is
|
||||
// holdable on this machine through whichever accounting path is armed (the
|
||||
// discard interaction is the case above's business, measured separately).
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram();
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u)
|
||||
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
|
||||
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
|
||||
}
|
||||
|
||||
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
|
||||
// captured one, then another XFB-inactive one. The GENERATED answer must
|
||||
// accumulate across the two accounting paths the armed reroute splits them
|
||||
// into (stream slots for the captured draw, statistics slots for the
|
||||
// others), and WRITTEN must stay exactly the captured draw's count - the
|
||||
// pairing the stream path exists to keep exact. Undiscarded, so the
|
||||
// accumulation invariant is holdable on this machine (see the discard
|
||||
// case's comment); the triangles rasterize into the harness framebuffer,
|
||||
// which nothing here reads.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
||||
const GLuint generated = QueryGenerated([]() {
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
|
||||
glEndTransformFeedback();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
|
||||
});
|
||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||
|
||||
GLuint written = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
|
||||
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
||||
}
|
||||
|
||||
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
|
||||
//
|
||||
// glPauseTransformFeedback closes the capture without closing the span, so a
|
||||
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
|
||||
// exactly as silent for it as for a draw with no span at all - while
|
||||
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
|
||||
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
|
||||
// silences). The frontend does keep a CPU counter for paused draws, but it can
|
||||
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
|
||||
// these draws are the reroute's business like any other - and the trap on the
|
||||
// other side is counting them TWICE, once in each accounting.
|
||||
//
|
||||
// Each case measures the SAME draw twice: once with no span open at all (the
|
||||
// capability control - what this stack can count) and once inside the paused
|
||||
// span, and requires the two to agree. That differential is what makes these
|
||||
// cases falsifying rather than vacuous: a stack where no counter reaches a
|
||||
// capture-less draw fails the control and skips, while a stack that counts the
|
||||
// unpaused draw and answers 0 for the paused one - which is what excluding
|
||||
// paused draws from the reroute produced - fails, instead of skipping into
|
||||
// green.
|
||||
|
||||
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
|
||||
// CPU delta, this reads 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
|
||||
"draws are CPU-counted on top of whatever the stream query says";
|
||||
}
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half of the comparison proves nothing; the "
|
||||
"bring-up probe measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "one triangle drawn while the capture span is paused is still one primitive "
|
||||
"generated - counted once, by whichever accounting owns it, never by two of them "
|
||||
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
|
||||
}
|
||||
|
||||
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
|
||||
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
|
||||
// the CTS's tessellator-measuring shape. Excluding paused draws from the
|
||||
// reroute left this counted by nothing at all on the affected device.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
|
||||
"on this stack, so the paused half proves nothing; the bring-up probe "
|
||||
"measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u)
|
||||
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
|
||||
"exactly one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "pausing the capture does not stop the tessellator from generating that triangle, "
|
||||
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
|
||||
"patch draw left out of the reroute is counted by nothing at all";
|
||||
}
|
||||
|
||||
// The other half of the same hole: the instanced entry points never reach the
|
||||
// frontend's paused accounting either, so a paused instanced draw excluded from
|
||||
// the reroute is likewise counted by nothing.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused =
|
||||
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half proves nothing";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "four instances generate four primitives whether or not the capture span is "
|
||||
"paused, and no instanced entry point reaches the frontend's paused accounting";
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||
// is green here whether the reroute ran or not (that is the "two pools
|
||||
// agree" point), so none of them can say the pinned lane actually exercised
|
||||
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
|
||||
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
|
||||
// XFB-inactive draw inside a GENERATED span must make the renderer say -
|
||||
// through its latched MGLOG_I - that it engaged the reroute. It reads
|
||||
// MG_Config not at all (on Android this module links the shipping library)
|
||||
// and trusts only the log bytes appended after it started.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
|
||||
"is what the PrimGenReroute. ctest entry does with "
|
||||
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
|
||||
"decides and this machine's verdict is its own business";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it rerouted anything; the PrimGenReroute. ctest "
|
||||
"entry sets both";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
// Taken BEFORE the draw, so the line this looks for can only be one this
|
||||
// process wrote for this span. The latch fires on the FIRST rerouted
|
||||
// draw, which is inside the query below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
|
||||
"a GENERATED query, and the renderer never reported engaging the reroute. The "
|
||||
"quirk is not armed - check the override mapping "
|
||||
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
|
||||
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,227 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RenderbufferBlendFormatScenario.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 - BLENDING WORKS ON A RENDERBUFFER WHOSE GL FORMAT HAS NO EXACT VkFormat.
|
||||
//
|
||||
// DirectVulkan force-disables blending on an attachment whose VkFormat lacks
|
||||
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT, which is the right thing to do - blending on such a
|
||||
// format is invalid pipeline state. The probe has to ask about the format the attachment ACTUALLY
|
||||
// has, and for renderbuffers it asked a different question from the one that created the image: the
|
||||
// image comes from ResolveTextureFormatInfo (which widens GL formats with no Vulkan twin onto a real
|
||||
// one) while the probe used the strict 1:1 converter, which answers VK_FORMAT_UNDEFINED for RGBA2,
|
||||
// RGBA12, RGB10, RGB12, RGB16 and the three-channel formats, and the 16-bit packed formats for RGBA4
|
||||
// and RGB5_A1.
|
||||
//
|
||||
// VkFormatProperties for VK_FORMAT_UNDEFINED are all zero, so the probe concluded "not blendable"
|
||||
// and every pipeline for that attachment was built with blendEnable = VK_FALSE - permanently, and
|
||||
// silently apart from one log line. The source colour then overwrites the destination instead of
|
||||
// blending with it, which is a wrong PICTURE, not a wrong error code.
|
||||
//
|
||||
// GL_RGB8 is the ordinary shape and is what this scenario leads with: it is a required
|
||||
// colour-renderable format, its image has been R8G8B8A8_UNORM all along, and the probe asked about
|
||||
// the 24-bit R8G8B8_UNORM that most drivers do not support at all. GL_RGBA4 covers the other half -
|
||||
// a format whose probe answered a real-but-different VkFormat.
|
||||
//
|
||||
// DirectGLES is the control: it forwards the renderbuffer to the ES driver and blends whatever the
|
||||
// driver blends, so a disagreement between the two backends is the defect.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = uColor;
|
||||
}
|
||||
)";
|
||||
|
||||
class RenderbufferBlendFormatScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << "program did not build: " << error;
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
Destroy();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns false (having skipped, not failed) when the driver will not give us a complete
|
||||
// framebuffer for this format - GL only requires a subset of formats to be
|
||||
// colour-renderable, and the point of the scenario is blending, not format support.
|
||||
bool MakeTarget(GLenum internalFormat) {
|
||||
Destroy();
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
return status == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void DrawColor(float r, float g, float b, float a) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uColor"), r, g, b, a);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// One draw of opaque black, then a 50%-alpha white draw over it with the ordinary
|
||||
// SRC_ALPHA / ONE_MINUS_SRC_ALPHA function. Blending gives mid-grey; a pipeline built with
|
||||
// blendEnable = VK_FALSE gives white, because the source simply overwrites.
|
||||
//
|
||||
// The tolerance is wide on purpose: RGBA4 has four bits per channel, so "mid-grey" is one of
|
||||
// a handful of representable values and the test must not become a quantisation test.
|
||||
void ExpectBlendedRatherThanOverwritten(const char* what) {
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty()) << what;
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(int(centre.r), 40) << what << ": got " << centre << ", which is darker than a blend of "
|
||||
"black and 50% white";
|
||||
EXPECT_LT(int(centre.r), 215) << what << ": got " << centre
|
||||
<< ", which is the source colour - blending was disabled";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The ordinary case, and the one broken today rather than only after the format table was
|
||||
// unified: a three-channel colour renderbuffer. Its image has been R8G8B8A8_UNORM all along while
|
||||
// the blend probe asked about R8G8B8_UNORM, which most drivers do not support at all.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAThreeChannelRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGB8)) GTEST_SKIP() << "GL_RGB8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGB8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The other half: a format whose strict converter answers a real-but-different VkFormat
|
||||
// (R4G4B4A4_UNORM_PACK16) while the image is R8G8B8A8_UNORM. Blend support for the packed 16-bit
|
||||
// formats is optional in Vulkan, so the probe could legitimately answer "no" for a format the
|
||||
// attachment does not have.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnALowBitPackedRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA4)) GTEST_SKIP() << "GL_RGBA4 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA4");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The control that keeps both of the above honest: the same sequence on the format whose probe
|
||||
// and image always agreed. If this one ever fails, the scenario is measuring the blend setup
|
||||
// rather than the format resolution.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAnRgba8Renderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA8)) GTEST_SKIP() << "GL_RGBA8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,178 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.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 - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
|
||||
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
|
||||
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
|
||||
// glSampleMaski last wrote.
|
||||
//
|
||||
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
|
||||
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
|
||||
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
|
||||
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
|
||||
// a fully black draw:
|
||||
//
|
||||
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
|
||||
// ... render ...
|
||||
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
|
||||
//
|
||||
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
|
||||
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
|
||||
// shape every application uses.
|
||||
//
|
||||
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
|
||||
// one asserts that the mask changed nothing.
|
||||
|
||||
#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 int kFboSize = 32;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kGreenFragmentSource = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampleMaskScopeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
// Process-wide GL state: leaving it set would hand the next scenario in this
|
||||
// process the very bug under test.
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
void ExpectQuadStillPaints(const char* what) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
|
||||
<< what << ": an all-black target means the sample mask discarded every fragment, "
|
||||
<< "which GL says it cannot do on a single-sample framebuffer";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
|
||||
// masked off if the mask is applied at all.
|
||||
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
|
||||
}
|
||||
|
||||
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
|
||||
// set.
|
||||
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
|
||||
}
|
||||
|
||||
// Control: the same mask word with the capability disabled has never had any effect, so this
|
||||
// one passed before the fix too. It is here so a regression that ignores the enable bit
|
||||
// instead of the sample count is still caught.
|
||||
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
|
||||
}
|
||||
|
||||
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
|
||||
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
|
||||
// mask change, both to the same single-sample target, both required to paint.
|
||||
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
ExpectQuadStillPaints("first draw, full mask");
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("second draw, mask 0x2");
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
|
||||
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
|
||||
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
|
||||
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
|
||||
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
GLint words = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
|
||||
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
|
||||
for (GLint word = 0; word < words; ++word) {
|
||||
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
|
||||
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,274 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleVariablesScenario.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_NumSamples REACHES THE SHADER, AND IT FOLLOWS THE DRAW FRAMEBUFFER.
|
||||
//
|
||||
// glslang declares gl_NumSamples only when it is NOT targeting SPIR-V - both the desktop and the
|
||||
// ES branch of Initialize.cpp wrap `uniform int gl_NumSamples;` in `if (spvVersion.spv == 0)`,
|
||||
// because SPIR-V has no NumSamples builtin to lower it to - and MobileGL always targets SPIR-V.
|
||||
// Every fragment shader that read the built-in therefore died at COMPILE time with
|
||||
// "'gl_NumSamples' : undeclared identifier", which is all 144 KHR-GL46.sample_variables.mask.*
|
||||
// bodies plus their es_31_compatibility twins.
|
||||
//
|
||||
// The source pipeline now lowers it onto a reserved default-block uniform and the draw path writes
|
||||
// the current draw framebuffer's sample count into it. Two claims, and the second is the one a
|
||||
// compile-only test cannot make: the value must be the DRAW FRAMEBUFFER's, so one program drawn
|
||||
// into a multisample target and then into a single-sample target has to report both counts. A
|
||||
// link-time bake would pass the first assertion and fail the second, which is exactly why the
|
||||
// write lives per draw.
|
||||
//
|
||||
// llvmpipe and lavapipe both offer 4x multisample RGBA8, so this runs for real in CI rather than
|
||||
// skipping; the skips below are for a driver that offers no multisample renderbuffer at all.
|
||||
|
||||
#include <algorithm>
|
||||
#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 const char* kVS = R"(#version 400 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// gl_NumSamples scaled so each count lands on its own well-separated 8-bit value: 1 -> 16,
|
||||
// 2 -> 32, 4 -> 64. Every sample of the fragment gets the same colour, so the resolve blit
|
||||
// averages identical values and the readback is exact rather than approximate.
|
||||
constexpr const char* kFS = R"(#version 400 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(float(gl_NumSamples) * (16.0 / 255.0), 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class SampleVariablesScenario : public ScenarioTest {};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SampleVariablesScenario, GlNumSamplesFollowsTheDrawFramebuffersSampleCount) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
// The compile failure this scenario exists for lands here, with glslang's own text.
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// ---- multisample target ----
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
// What the driver actually allocated - a request is a lower bound, and the shader has to
|
||||
// agree with the query rather than with what was asked for.
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
ASSERT_GE(realizedSamples, 2) << "the multisample framebuffer reports GL_SAMPLES " << realizedSamples;
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Resolve into the default framebuffer to read it back.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0) << " into a " << realizedSamples
|
||||
<< "-sample framebuffer; 1 means the reserved uniform was never written, 0 means it was "
|
||||
<< "written but never uploaded";
|
||||
}
|
||||
gl.EndFrame();
|
||||
|
||||
// ---- the SAME program into a single-sample target ----
|
||||
// A link-time bake of the sample count would keep reporting the multisample value here.
|
||||
GLuint ssFbo = 0, ssRbo = 0;
|
||||
glGenFramebuffers(1, &ssFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, ssFbo);
|
||||
glGenRenderbuffers(1, &ssRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, ssRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ssRbo);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image single = ReadPixels(width, height);
|
||||
const Rgba8 centre = single.At(width / 2, height / 2);
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is ONE for a non-multisample framebuffer, where
|
||||
// glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
EXPECT_NEAR(centre.r, 16, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0)
|
||||
<< " into a single-sample framebuffer; the value is a property of the DRAW FRAMEBUFFER, "
|
||||
<< "so re-using the program must re-write it";
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteRenderbuffers(1, &ssRbo);
|
||||
glDeleteFramebuffers(1, &ssFbo);
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// ARB_sample_shading is advertised, and until now glMinSampleShading was a logging no-op while
|
||||
// glEnable(GL_SAMPLE_SHADING) fell out of RenderState::SetCapability's default arm - so an
|
||||
// application could ask for a shading rate and get silence from both halves.
|
||||
//
|
||||
// What this can and cannot assert. The RATE itself is not observable from a portable shader:
|
||||
// GL 4.6 core 14.3.1 makes any use of gl_SampleID or gl_SamplePosition force per-sample
|
||||
// evaluation on its own, so the very built-ins that would report the rate defeat the
|
||||
// measurement. What IS worth pinning is that the state now reaches both backends without
|
||||
// damage: DirectGLES forwards glEnable(GL_SAMPLE_SHADING) + glMinSampleShading to the ES
|
||||
// driver (and must not, on a driver that has neither, push an INVALID_ENUM into the
|
||||
// application's error queue), and DirectVulkan bakes sampleShadingEnable/minSampleShading into
|
||||
// a NEW pipeline - which it may only do with the device's sampleRateShading feature enabled.
|
||||
TEST_F(SampleVariablesScenario, SampleShadingStateReachesTheBackendWithoutDisturbingTheDraw) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; sample shading needs a multisample target";
|
||||
}
|
||||
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glViewport(0, 0, width, height);
|
||||
|
||||
glEnable(GL_SAMPLE_SHADING);
|
||||
glMinSampleShading(1.0f);
|
||||
EXPECT_EQ(glIsEnabled(GL_SAMPLE_SHADING), static_cast<GLboolean>(GL_TRUE));
|
||||
GLfloat rate = -1.0f;
|
||||
glGetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &rate);
|
||||
EXPECT_FLOAT_EQ(rate, 1.0f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "enabling sample shading raised a GL error";
|
||||
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sample-shading draw raised a GL error";
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
// The rate changes how OFTEN the shader runs, never what it computes - so the same
|
||||
// gl_NumSamples reading has to come back.
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "the draw changed its result once sample shading was enabled";
|
||||
|
||||
glMinSampleShading(0.0f);
|
||||
glDisable(GL_SAMPLE_SHADING);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,231 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampledSetStalenessScenario.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 - A TEXTURE THAT BECOMES COMPLETE WITHOUT A REBIND MUST RE-ENTER THE SAMPLED SET.
|
||||
//
|
||||
// DirectVulkan does not bind a texture GL calls incomplete: it substitutes a fallback so the
|
||||
// sampler reads (0,0,0,1) instead of losing the draw. That decision is made twice per draw - once
|
||||
// by CollectSampledTextures, which builds the list SetupDraw syncs, materialises pending clears
|
||||
// for and transitions to a sampled layout BEFORE the render pass opens, and once by the descriptor
|
||||
// resolve inside the pass. Both ask SamplesAsIncompleteTexture.
|
||||
//
|
||||
// The per-draw memo that lets the first of those be skipped was keyed only on the program, the
|
||||
// transform flags and the texture BIND generation. Completeness is not a function of any of them:
|
||||
// it moves on a filter change (glTexParameteri / glSamplerParameteri), on a level-range change,
|
||||
// and on an upload that fills the mip chain - none of which bind anything. So a texture that went
|
||||
// incomplete -> complete under a fixed binding kept being answered out of the memo as "not in the
|
||||
// set", and the work SetupDraw does for the set never happened for it:
|
||||
//
|
||||
// * its queued clear was never materialised, so the draw sampled pre-clear content - wrong
|
||||
// pixels, no validation layer needed, which is what the case below detects; and
|
||||
// * its layout transition moved into the descriptor resolve, which records
|
||||
// vkCmdPipelineBarrier inside an already-open render pass whose subpass declares no
|
||||
// self-dependency - the exact hazard CollectSampledTextures exists to prevent.
|
||||
//
|
||||
// The fix adds the sampling-resolution generation to that memo key, which is the counter the
|
||||
// codebase already maintains for "what a unit resolves to changed without a bind" and which both
|
||||
// TextureObjectBase::BumpShapeVersion and SamplerObject::BumpVersion move.
|
||||
|
||||
#include <cstddef>
|
||||
#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 kFboSize = 32;
|
||||
constexpr int kTexSize = 8;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// texelFetch, not texture(): the point is WHICH image is sampled, and a fetch cannot be
|
||||
// explained away by filtering.
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_tex;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = texelFetch(u_tex, ivec2(0, 0), 0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampledSetStalenessScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
// The sampled texture: ONE level, and no glTexParameteri at all, so MIN_FILTER
|
||||
// keeps its initial GL_NEAREST_MIPMAP_LINEAR and GL calls it mipmap-incomplete.
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
std::vector<unsigned char> green(static_cast<std::size_t>(kTexSize * kTexSize * 4), 0);
|
||||
for (std::size_t i = 0; i < green.size(); i += 4) {
|
||||
green[i + 1] = 255;
|
||||
green[i + 3] = 255;
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
green.data());
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// One draw of the fullscreen quad sampling texel (0,0) of whatever unit 0 holds, and
|
||||
// NO readback. That matters: a readback submits and waits, which ends the command
|
||||
// buffer and resets the per-draw memos with it - so a case that read back between its
|
||||
// two draws would never leave a stale entry to catch. The two draws here have to land
|
||||
// in one recording.
|
||||
void DrawOnly() {
|
||||
BindFbo(m_target);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const GLint location = glGetUniformLocation(m_program, "u_tex");
|
||||
if (location != -1) glUniform1i(location, 0);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw raised a GL error";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_texture = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The full sequence, ordered so the ONLY state change between the two draws is the filter.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenAFilterChangeCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// 1. Queue a clear on the texture through an FBO and take it straight back out, with no
|
||||
// draw in between - the "attach -> clear -> detach" shape that leaves the clear
|
||||
// pending for whoever samples the texture next.
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
|
||||
// 2. Draw while the texture is still incomplete. The backend substitutes its fallback,
|
||||
// and the per-draw memo records the resulting sampled set.
|
||||
DrawOnly();
|
||||
|
||||
// 3. Make it complete. No bind, no upload, no program change - one filter write, which is
|
||||
// exactly the state the old memo key could not see.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the filter raised a GL error";
|
||||
|
||||
// 4. Draw again, into the same recording, and only now read back. The texture is in the
|
||||
// sampled set now, so its queued clear has to be materialised before the pass opens and
|
||||
// the fetch has to see RED. Reading the green the texture was uploaded with means the
|
||||
// clear was never materialised, i.e. the texture never entered the set - the stale-memo
|
||||
// bug. Black means the fallback was still being handed out.
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the completeness flip"))
|
||||
<< "green means the queued clear was never materialised, so the texture never re-entered "
|
||||
"the sampled set after the filter change; blue means the draw did not happen at all";
|
||||
}
|
||||
|
||||
// The same flip driven from a SAMPLER OBJECT rather than the texture's own parameters. It is
|
||||
// the other half of what feeds the completeness predicate, it moves the same generation, and
|
||||
// it likewise binds nothing.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenASamplerObjectCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint sampler = 0;
|
||||
glGenSamplers(1, &sampler);
|
||||
// Bound BEFORE the first draw, still carrying the mipmapping default, so binding it is
|
||||
// not what changes between the two draws.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glBindSampler(0, sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object raised a GL error";
|
||||
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawOnly();
|
||||
|
||||
// One parameter write on an ALREADY-BOUND sampler object.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sampler filter raised a GL error";
|
||||
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the sampler-object flip"))
|
||||
<< "green means the queued clear was never materialised after the sampler parameter change";
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &sampler);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,339 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.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 - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END.
|
||||
//
|
||||
// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and
|
||||
// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments,
|
||||
// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM.
|
||||
// Every gl_spirv conformance body died on the first of those two calls.
|
||||
//
|
||||
// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two
|
||||
// modules that glslang compiled ahead of time (embedded below as words, so the test depends on
|
||||
// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel
|
||||
// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe.
|
||||
//
|
||||
// The two specialization constants are the load-bearing part. The vertex module scales its
|
||||
// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant
|
||||
// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a
|
||||
// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of
|
||||
// green), and BOTH would show up in the readback. A "specialization" that merely stored the
|
||||
// values without folding them in is exactly the failure mode this shape is built to catch.
|
||||
//
|
||||
// The GLSL the modules came from:
|
||||
// vertex: layout(location = 0) in vec2 aPos;
|
||||
// layout(constant_id = 3) const float uScale = 1.0;
|
||||
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
|
||||
// fragment: layout(location = 0) out vec4 oColor;
|
||||
// layout(constant_id = 7) const int uChannel = 0;
|
||||
// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; }
|
||||
// compiled with `glslangValidator -G --target-env opengl`.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V
|
||||
#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551
|
||||
#endif
|
||||
#ifndef GL_SPIR_V_BINARY
|
||||
#define GL_SPIR_V_BINARY 0x9552
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
class SpirvShaderBinaryScenario : public ScenarioTest {};
|
||||
|
||||
// 255 words
|
||||
const unsigned int kVertexModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
|
||||
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
|
||||
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
|
||||
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
|
||||
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
|
||||
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
|
||||
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
|
||||
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
|
||||
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
|
||||
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
|
||||
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
|
||||
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
|
||||
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
|
||||
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
|
||||
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
|
||||
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
|
||||
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
|
||||
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
|
||||
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
|
||||
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
|
||||
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
|
||||
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
|
||||
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
// 134 words
|
||||
const unsigned int kFragmentModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u,
|
||||
0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u,
|
||||
0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u,
|
||||
0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
|
||||
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
|
||||
0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au,
|
||||
0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u,
|
||||
0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u,
|
||||
0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
|
||||
0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u,
|
||||
0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u,
|
||||
0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
|
||||
// The quad the vertex module transforms. Full-viewport before the scale, so a scale of
|
||||
// 0.5 covers exactly the middle half of each axis and the corners stay background.
|
||||
const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
// The specialization constant ids the two modules declare.
|
||||
constexpr unsigned int kScaleConstantId = 3;
|
||||
constexpr unsigned int kChannelConstantId = 7;
|
||||
|
||||
unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount,
|
||||
unsigned int constantId, unsigned int constantValue, std::string* outLog) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words,
|
||||
static_cast<GLsizei>(wordCount * sizeof(unsigned int)));
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
if (outLog) *outLog = "glShaderBinary rejected the module";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLint isSpirv = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) {
|
||||
if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
glSpecializeShader(shader, "main", 1, &constantId, &constantValue);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled != GL_TRUE) {
|
||||
if (outLog) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
*outLog = std::string(log.data());
|
||||
}
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
|
||||
GLint formatCount = -1;
|
||||
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format";
|
||||
|
||||
std::vector<GLint> formats(static_cast<size_t>(formatCount), 0);
|
||||
glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
|
||||
<< "the count and the list have to describe the same thing";
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
// 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but
|
||||
// GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently.
|
||||
glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule));
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_ENUM));
|
||||
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION))
|
||||
<< "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader";
|
||||
|
||||
// glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with
|
||||
// it - the transition the conformance suite checks explicitly.
|
||||
const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE);
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializeShaderErrorSurfaceMatchesTheExtension) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// 4242 is not one of the module's constant ids. ARB_gl_spirv enumerates that as
|
||||
// GL_INVALID_VALUE, and an erroring GL command has no other effect - so the shader is left
|
||||
// untouched rather than pushed into a failed-compile state.
|
||||
const unsigned int badId = 4242;
|
||||
const unsigned int value = 0;
|
||||
glSpecializeShader(shader, "main", 1, &badId, &value);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Same for an entry point the module does not carry.
|
||||
glSpecializeShader(shader, "notMain", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Neither refusal specialized the shader, so a well-formed call still works.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE);
|
||||
|
||||
// But a SECOND specialization of a shader that HAS been specialized is INVALID_OPERATION
|
||||
// until glShaderBinary re-associates the module.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "re-associating the module makes specialization legal again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 16);
|
||||
ASSERT_GE(height, 16);
|
||||
|
||||
std::string log;
|
||||
// Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies.
|
||||
unsigned int halfBits = 0;
|
||||
const float half = 0.5f;
|
||||
std::memcpy(&halfBits, &half, sizeof(halfBits));
|
||||
|
||||
const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule,
|
||||
sizeof(kVertexModule) / sizeof(kVertexModule[0]),
|
||||
kScaleConstantId, halfBits, &log);
|
||||
ASSERT_NE(vs, 0u) << "vertex: " << log;
|
||||
// Channel 1 is green; the module's own default is 0 (red), so a specialization that did
|
||||
// nothing paints the wrong colour.
|
||||
const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule,
|
||||
sizeof(kFragmentModule) / sizeof(kFragmentModule[0]),
|
||||
kChannelConstantId, 1u, &log);
|
||||
ASSERT_NE(fs, 0u) << "fragment: " << log;
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> programLog(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(programLog.size()), nullptr, programLog.data());
|
||||
FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data();
|
||||
}
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.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);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied";
|
||||
EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted";
|
||||
|
||||
// A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the
|
||||
// clear colour - which is what proves constant id 3 reached the vertex module. At the
|
||||
// default scale of 1.0 the quad covers the whole viewport and this pixel would be green.
|
||||
const Rgba8 corner = painted.At(1, 1);
|
||||
EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied";
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,896 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.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 - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
|
||||
//
|
||||
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
|
||||
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
|
||||
// case here is the reduced form of a conformance body that fails on a device:
|
||||
//
|
||||
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
|
||||
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
|
||||
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
|
||||
// two backends reach it by completely different routes - DirectGLES has to name a
|
||||
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
|
||||
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
|
||||
//
|
||||
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
|
||||
// user-declared per-vertex interface block, both read back out of gl_in[] by the
|
||||
// evaluation stage and only then captured. This is the shape of
|
||||
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
|
||||
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
|
||||
// the family still fails: gl_Position arrives, and everything travelling beside it in
|
||||
// the same patch does not.
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
|
||||
// absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure instead of looking like an ordinary numeric mismatch.
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class TessellationXfbCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static GLint MaxPatchVertices() {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
|
||||
DrainErrors();
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over a single patch. Returns the capture buffer read back as
|
||||
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
|
||||
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
|
||||
float expected, float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture never reached "
|
||||
<< "these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// Defined below the shader builders it uses. `withPointSize` is the conformance
|
||||
// body's own should_pass_pointsize_data axis.
|
||||
void RunPerVertexPayloadCase(bool withPointSize);
|
||||
|
||||
// Why the gl_PointSize cases cannot be run here, or empty when they can.
|
||||
//
|
||||
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
|
||||
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
|
||||
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
|
||||
// has no query that reports either, so this probes for it by running a program.
|
||||
//
|
||||
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
|
||||
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
|
||||
// compares that against the identical program without the write. A backend that
|
||||
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
|
||||
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
|
||||
// comes back untouched too - which is a capability answer, not a capture answer. If
|
||||
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
|
||||
// the cases run and FAIL rather than skipping on an unrelated breakage.
|
||||
//
|
||||
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
|
||||
// a `return`, so a void helper would leave only the helper and let the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyPointSizeCasesCannotRun();
|
||||
|
||||
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
|
||||
// extensions are independent (Loader models them as two PointSizeTier fields fed by
|
||||
// four distinct strings, and neither implies the other), so a driver with
|
||||
// tessellation point size and no geometry point size passes the probe above and
|
||||
// still cannot run the case below. Same two-program shape, one stage over.
|
||||
//
|
||||
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
|
||||
// a hardcoded frontend constant (256) with no capability behind it, so "does this
|
||||
// stack have a geometry stage at all" can only be answered by trying to build one -
|
||||
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
|
||||
// reason.
|
||||
std::string WhyGeometryPointSizeCaseCannotRun();
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Built-ins captured BY NAME from the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
const char* const kMinimalVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kMinimalTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Values no stale buffer would hold by accident. The two sources differ ONLY by
|
||||
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
|
||||
// built-in is not even declared in a tessellation stage without
|
||||
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
|
||||
const char* const kPositionTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
gl_PointSize = 5.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The two probe programs. They differ by one statement; both capture `probe_value`,
|
||||
// which has nothing to do with point size.
|
||||
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 3.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const auto probeCaptures = [&](const char* tessEvalSource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"probe_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
|
||||
DrainErrors();
|
||||
return captured[0] == 42.0f;
|
||||
};
|
||||
|
||||
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
|
||||
if (withPointSize) return {};
|
||||
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
|
||||
// The control failed too, so nothing here is about point size.
|
||||
return {};
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
|
||||
"program captures an ordinary varying with the gl_PointSize write removed and captures "
|
||||
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
|
||||
"Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
// point_mode with every level at 1 emits three points, all carrying the same
|
||||
// constant; only the first record has to be right for the mechanism to be proven.
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The per-vertex payload the control stage hands to the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body's own shapes, reduced to one patch and parameterised by the
|
||||
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
|
||||
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
|
||||
// which it varies together with point_mode - and which decides whether the whole
|
||||
// program even involves the per-vertex built-in that ESSL gates behind an extension.
|
||||
std::string PayloadVertexSource(bool withPointSize) {
|
||||
return R"(#version 420 core
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
|
||||
R"(};
|
||||
void main()
|
||||
{
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(vertices = )" + std::to_string(outputVertices) +
|
||||
R"() out;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_out[];
|
||||
out OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} result[];
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize
|
||||
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
|
||||
: "") +
|
||||
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
|
||||
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
|
||||
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
|
||||
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
|
||||
gl_InvocationID + 3, gl_InvocationID + 4);
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
|
||||
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
|
||||
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
|
||||
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(isolines, equal_spacing, ccw, point_mode) in;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(};
|
||||
in OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} tc_data[];
|
||||
|
||||
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
|
||||
R"(out vec4 te_position;
|
||||
out vec2 te_value1;
|
||||
out flat ivec4 te_value2;
|
||||
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
|
||||
R"( te_position = vec4 (0.0);
|
||||
te_value1 = vec2 (0.0);
|
||||
te_value2 = ivec4(0);
|
||||
|
||||
for (int n = 0; n < )" + std::to_string(inputVertices) +
|
||||
R"(; ++n)
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
|
||||
R"( te_position += gl_in [n].gl_Position;
|
||||
te_value1 += tc_data[n].value1;
|
||||
te_value2 += tc_data[n].value2;
|
||||
}
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// The reduced conformance body. `withPointSize` selects between its two halves;
|
||||
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
|
||||
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
|
||||
// evaluation stage - is the same on both.
|
||||
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (withPointSize) {
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
}
|
||||
const GLint patchVertices = MaxPatchVertices();
|
||||
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
|
||||
|
||||
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
|
||||
// the control stage runs that many invocations and every one of them contributes.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
|
||||
if (withPointSize) varyings.push_back("te_pointsize");
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
|
||||
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
|
||||
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
varyings);
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
float referencePointSize = 0.0f;
|
||||
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
float referenceValue1[2] = {0.0f, 0.0f};
|
||||
int referenceValue2[4] = {0, 0, 0, 0};
|
||||
for (int n = 0; n < patchVertices; ++n) {
|
||||
referencePointSize += 1.0f / static_cast<float>(n + 1);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
referencePosition[c] += static_cast<float>(n * 4 + c);
|
||||
referenceValue2[c] += n + 1 + c;
|
||||
}
|
||||
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
|
||||
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
|
||||
}
|
||||
|
||||
// isolines with every level at 1 emits two points; the record stride is
|
||||
// vec4 + vec2 + ivec4 [+ float] components.
|
||||
const std::size_t stride = withPointSize ? 11 : 10;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
|
||||
<< "te_position." << c << " (gl_in[].gl_Position)";
|
||||
}
|
||||
for (int c = 0; c < 2; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
|
||||
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
|
||||
}
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const std::size_t index = static_cast<std::size_t>(6 + c);
|
||||
ASSERT_LT(index, captured.size());
|
||||
int actual = 0;
|
||||
std::memcpy(&actual, &captured[index], sizeof(actual));
|
||||
EXPECT_EQ(actual, referenceValue2[c])
|
||||
<< "te_value2." << c << " (the user per-vertex block's integer member)";
|
||||
}
|
||||
if (withPointSize) {
|
||||
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
|
||||
<< "te_pointsize (gl_in[].gl_PointSize)";
|
||||
}
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
|
||||
RunPerVertexPayloadCase(false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same built-in, one stage over.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
|
||||
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
|
||||
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
|
||||
// extensions that do not imply each other, so the geometry arm is a second code path
|
||||
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
|
||||
// shader, so without this case the arm ships untested.
|
||||
const char* const kPointSizeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = 4.0;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
// The control: identical but for the gl_PointSize write, so the pair answers "can this
|
||||
// stack host a geometry stage that names the built-in" without asking anything about
|
||||
// capture.
|
||||
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
|
||||
const auto probeCaptures = [&](const char* geometrySource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> poison(1, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
|
||||
GL_STATIC_DRAW);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
float captured = kPoison;
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
|
||||
&captured);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
DrainErrors();
|
||||
return captured == 7.0f;
|
||||
};
|
||||
|
||||
if (probeCaptures(kPointSizeGeometrySource)) return {};
|
||||
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
|
||||
// The control failed too, so this stack cannot run a capturing geometry stage at
|
||||
// all - which is not what this case is about, and is the question the dead
|
||||
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
|
||||
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
|
||||
// and goes red on its own if that is what actually broke.
|
||||
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
|
||||
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
|
||||
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
|
||||
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
|
||||
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
const std::vector<float> poison(2, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(2, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
|
||||
"the stage carrying it fails to compile";
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The conformance body's own READBACK, which is not glGetBufferSubData.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Every case above reads the capture back with glGetBufferSubData because that is the
|
||||
// shortest path to the bytes. The conformance bodies do something else: they respecify
|
||||
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
|
||||
// while it is simultaneously bound to indexed capture point 0, and then read it with
|
||||
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
|
||||
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
|
||||
// call, so the sequence itself is worth pinning: none of the map path's error conditions
|
||||
// may fire, and the mapped bytes must be the captured ones.
|
||||
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
|
||||
for (int iteration = 0; iteration < 2; ++iteration) {
|
||||
// Respecified through the generic binding, exactly as the conformance body does,
|
||||
// while the same buffer is still bound to capture point 0.
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
|
||||
|
||||
const auto* mapped =
|
||||
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
|
||||
GL_MAP_READ_BIT));
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
|
||||
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
|
||||
const std::vector<float> captured(mapped, mapped + kFloats);
|
||||
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
|
||||
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
|
||||
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
|
||||
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
|
||||
// without asking for it does not merely lose the value - the stage fails to compile
|
||||
// and the whole program is replaced by program 0.
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
|
||||
RunPerVertexPayloadCase(true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same capture through a PROGRAM PIPELINE OBJECT.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body runs each of its configurations twice: once with a monolithic
|
||||
// program object and once with a pipeline of four separable programs, the capture
|
||||
// declared on the separable EVALUATION program. That second shape goes through the
|
||||
// hidden composite the pipeline object builds for the draw, and it is the only place a
|
||||
// tessellation capture and the composite meet - so the capture list has to survive being
|
||||
// taken from a program that is not the one bound.
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
// One separable program per stage. Only the evaluation program carries the capture
|
||||
// list, because it is the one whose outputs are captured.
|
||||
const auto buildSeparable = [&](GLenum stage, const char* source,
|
||||
const std::vector<const char*>& varyings) -> GLuint {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
glAttachShader(program, shader);
|
||||
if (!varyings.empty()) {
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(shader);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
};
|
||||
|
||||
m_buildLog.clear();
|
||||
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
|
||||
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
|
||||
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
|
||||
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
|
||||
const GLuint evalProgram =
|
||||
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
|
||||
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
|
||||
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
|
||||
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
|
||||
|
||||
GLuint pipeline = 0;
|
||||
glGenProgramPipelines(1, &pipeline);
|
||||
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
|
||||
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(0);
|
||||
glBindProgramPipeline(pipeline);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(kFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glBindProgramPipeline(0);
|
||||
glDeleteProgramPipelines(1, &pipeline);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,908 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.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
|
||||
//
|
||||
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
|
||||
//
|
||||
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
|
||||
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
|
||||
// application's capability check passes, it takes the texture-view path, and the view texture it
|
||||
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
|
||||
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
|
||||
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
|
||||
// that aliased nothing.
|
||||
//
|
||||
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
|
||||
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
|
||||
// * writes through either name are visible through the other (CoherencyIsBidirectional), and
|
||||
// * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
|
||||
// entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
|
||||
// parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
|
||||
// GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
|
||||
//
|
||||
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
|
||||
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
|
||||
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
|
||||
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
|
||||
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
|
||||
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
|
||||
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
|
||||
// see NoExtensionSupportIsRefusedRatherThanFaked.
|
||||
//
|
||||
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
|
||||
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
|
||||
// through the two names, or a value that changed after a write through the other name.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#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 kSize = 64;
|
||||
// The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
|
||||
// uniform image would otherwise pass a test that only ever looked at covered texels.
|
||||
constexpr int kUncoveredTop = 16;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform vec4 uRect; // x0, y0, x1, y1 in NDC
|
||||
uniform float uDepth; // NDC z
|
||||
void main() {
|
||||
vec2 p = mix(uRect.xy, uRect.zw, aPos);
|
||||
gl_Position = vec4(p, uDepth, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
|
||||
// draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
|
||||
// COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
|
||||
// error or clobber attachment 0.
|
||||
constexpr const char* kCoverageFragmentSource = R"(#version 330 core
|
||||
layout (location = 0) out vec4 outColor;
|
||||
layout (location = 1) out float outUnused;
|
||||
void main() {
|
||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
outUnused = 1.0 / 255.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
|
||||
// D24S8 image through two GL texture names bound to two units in this one invocation.
|
||||
// `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
|
||||
// at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
|
||||
// it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
|
||||
constexpr const char* kShadingFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uCoverage; // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
|
||||
uniform sampler2D uDepthView; // the VIEW, DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
|
||||
float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
|
||||
gl_FragDepth = depth;
|
||||
}
|
||||
)";
|
||||
|
||||
// Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
|
||||
// and unpacks the word back into the four bytes it was written as.
|
||||
constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uWords;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float((word ) & 0xFFu) / 255.0,
|
||||
float((word >> 8) & 0xFFu) / 255.0,
|
||||
float((word >> 16) & 0xFFu) / 255.0,
|
||||
float((word >> 24) & 0xFFu) / 255.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 330 core
|
||||
uniform sampler2D uTexture;
|
||||
uniform float uLod;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
|
||||
std::to_string(c.a) + ")";
|
||||
}
|
||||
|
||||
class TextureViewScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!TextureViewUsable()) {
|
||||
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
|
||||
<< " (GL_ARB_texture_view not advertised)";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint rbo : m_rbos) {
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
}
|
||||
m_rbos.clear();
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
if (m_vao != 0) {
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
if (m_vbo != 0) {
|
||||
glDeleteBuffers(1, &m_vbo);
|
||||
m_vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial same-format full-range view. It exercises nothing the cases below test,
|
||||
// so a backend that simply does not have the feature skips instead of failing every
|
||||
// one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
|
||||
bool TextureViewUsable() {
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint view = 0;
|
||||
glGenTextures(1, &view);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(1, &view);
|
||||
glDeleteTextures(1, &storage);
|
||||
return usable;
|
||||
}
|
||||
|
||||
GLuint MakeVao() {
|
||||
if (m_vao != 0) return m_vao;
|
||||
// A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
|
||||
// one buffer serves every draw here.
|
||||
static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
return m_vao;
|
||||
}
|
||||
|
||||
GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
|
||||
EXPECT_NE(program, 0u) << "program failed to build: " << error;
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint MakeFbo() {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
|
||||
// here views. Levels beyond 1 stay undefined until a caller fills them.
|
||||
GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
|
||||
const GLuint texture = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
|
||||
glUseProgram(program);
|
||||
glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
|
||||
const GLint depthLocation = glGetUniformLocation(program, "uDepth");
|
||||
if (depthLocation >= 0) glUniform1f(depthLocation, depth);
|
||||
glBindVertexArray(MakeVao());
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
// Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
|
||||
Image ReadFbo(GLuint fbo, int width, int height) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
return ReadPixels(width, height);
|
||||
}
|
||||
|
||||
// Every pixel of the inclusive region must match `expected` within `tolerance` per
|
||||
// channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
|
||||
// three of four vertices carrying stale data still paints a correct centre pixel.
|
||||
void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
|
||||
const char* what) {
|
||||
int offenders = 0;
|
||||
Rgba8 firstOffender{};
|
||||
int firstX = -1;
|
||||
int firstY = -1;
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const Rgba8 actual = image.At(x, y);
|
||||
const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
|
||||
std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
|
||||
std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
|
||||
std::abs(int(actual.a) - int(expected.a)) <= tolerance;
|
||||
if (!ok) {
|
||||
if (offenders == 0) {
|
||||
firstOffender = actual;
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
}
|
||||
++offenders;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
|
||||
<< (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
|
||||
<< ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
|
||||
<< Describe(expected) << " +/- " << tolerance;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
std::vector<GLuint> m_rbos;
|
||||
std::vector<GLuint> m_programs;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// The driving case: the Better Clouds full-mode pipeline, in its real order.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// --- Resources.java:230-251, in order ---------------------------------------------
|
||||
const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint coverage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
|
||||
const GLuint coverageFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
|
||||
const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
|
||||
glDrawBuffers(1, drawBuffers);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "the coverage framebuffer is incomplete; the mod would silently demote to its "
|
||||
"fallback configuration here (Resources.java:187-209)";
|
||||
|
||||
// The view is made from a name glGenTextures has only RESERVED - it has never been
|
||||
// bound, so glTextureView has to instantiate the texture object itself.
|
||||
const GLuint coverageDepthView = MakeTexture();
|
||||
glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";
|
||||
|
||||
// The two names must be distinguishable through the queries, or nothing below proves
|
||||
// which one produced a sample.
|
||||
GLint parentMode = 0;
|
||||
GLint viewMode = 0;
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
|
||||
EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
|
||||
<< "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
|
||||
"the parent is precisely what a texture view exists to avoid";
|
||||
|
||||
// --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
|
||||
const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
|
||||
ASSERT_NE(coverageProgram, 0u);
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
|
||||
glClearDepth(0.0);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilMask(0xff);
|
||||
glClearStencil(0);
|
||||
// The coverage COUNT: one increment per depth-passing cloud fragment.
|
||||
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
|
||||
glStencilFunc(GL_ALWAYS, 0xff, 0xff);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Quad 1 covers everything above the uncovered strip, at window depth 0.25.
|
||||
const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
|
||||
DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
|
||||
// Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
|
||||
// so it both passes the depth test and increments the stencil a second time.
|
||||
DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";
|
||||
|
||||
// --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
|
||||
// A different draw framebuffer, exactly as the mod does (it hands the frame back to
|
||||
// Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
|
||||
// being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
GLuint destinationDepth = 0;
|
||||
glGenRenderbuffers(1, &destinationDepth);
|
||||
m_rbos.push_back(destinationDepth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
// The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
|
||||
ASSERT_NE(shadingProgram, 0u);
|
||||
glUseProgram(shadingProgram);
|
||||
// Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
|
||||
// own unit assignment (Resources.java:309/311).
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
||||
DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";
|
||||
|
||||
const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
|
||||
// A stub view samples (0,0,0,1), which fails the green channel of both covered
|
||||
// regions; a view that inherited the parent's stencil aspect fails them too.
|
||||
constexpr int kTolerance = 3;
|
||||
ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
|
||||
"the uncovered strip must read coverage 0 and cleared depth 0");
|
||||
ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
|
||||
"one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
|
||||
ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
|
||||
kTolerance,
|
||||
"two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
|
||||
"through the view");
|
||||
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Storage sharing, in both directions. This is the assertion a copy-based emulation
|
||||
// fails, and the reason the no-EXT path refuses rather than emulates.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Render red through the PARENT's name...
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// ...and read it back through the VIEW's.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "a texture view must be attachable like any other texture";
|
||||
Image throughView = ReadFbo(viewFbo, kSize, kSize);
|
||||
ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
|
||||
"a write through the parent must be visible through the view");
|
||||
|
||||
// Now the other direction: write green through the VIEW, read through the PARENT.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const Image throughParent = ReadFbo(fbo, kSize, kSize);
|
||||
ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
|
||||
"a write through the view must be visible through the parent - they are one "
|
||||
"storage, not two");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Format reinterpretation within a view class (GL 4.6 core table 8.21).
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
|
||||
// other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
|
||||
// answer a fact about the BITS rather than about the colour.
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = 0x40;
|
||||
texels[i + 1] = 0x80;
|
||||
texels[i + 2] = 0xC0;
|
||||
texels[i + 3] = 0xFF;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";
|
||||
|
||||
// NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
|
||||
// so a test that only asserted the view's answer could not tell "the reinterpret is
|
||||
// wrong" from "the seed never reached the GPU at all". Read the same texels through
|
||||
// the parent's own format first.
|
||||
const GLuint parentFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image seeded = ReadFbo(parentFbo, kSize, kSize);
|
||||
ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
|
||||
"control: the storage must hold the seeded byte pattern before any view reads it");
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
GLint viewFormat = 0;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";
|
||||
|
||||
// A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
|
||||
// assertion below is that the view and this texture sample IDENTICALLY.
|
||||
//
|
||||
// Comparing against a reference texture rather than against a hard-coded colour is
|
||||
// deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
|
||||
// about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
|
||||
// with a raw-EGL program that reproduces the same wrong decode with NO view in play).
|
||||
// Holding both sides to the same driver factors that out completely: whatever the
|
||||
// driver makes of a usampler2D fetch, the view has to make the same thing of it, or
|
||||
// it is not delivering the storage's bits. A view that samples zero, that lands on
|
||||
// the wrong texels, or that lost its format still fails.
|
||||
constexpr std::uint32_t kExpectedWord = 0xFFC08040u; // little-endian A,B,G,R
|
||||
const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
|
||||
std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
|
||||
glBindTexture(GL_TEXTURE_2D, reference);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
const auto decodeThrough = [&](GLuint texture) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(glGetUniformLocation(program, "uWords"), 0);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
|
||||
return ReadFbo(destination.fbo, kSize, kSize);
|
||||
};
|
||||
|
||||
const Image throughReference = decodeThrough(reference);
|
||||
const Image throughView = decodeThrough(view);
|
||||
|
||||
// Guard against the degenerate agreement of two black images: the reference must
|
||||
// itself carry something, or "identical" would prove nothing.
|
||||
const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
|
||||
ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
|
||||
<< "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";
|
||||
|
||||
std::size_t mismatches = 0;
|
||||
for (int y = 0; y < kSize; ++y) {
|
||||
for (int x = 0; x < kSize; ++x) {
|
||||
if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(mismatches, 0u)
|
||||
<< "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
|
||||
"holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
|
||||
<< ", reference centre is " << Describe(referenceTexel);
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Sub-ranges: one mip level of two, and one layer of an array.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
|
||||
// Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
|
||||
glViewport(0, 0, kSize / 2, kSize / 2);
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
GLint immutableLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 1);
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
// GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
|
||||
EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";
|
||||
|
||||
// The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
|
||||
// the blue half-size image.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
|
||||
ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
|
||||
"the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
|
||||
}
|
||||
|
||||
TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kChosenLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// A different colour per layer, so a view that lost its layer offset reads the wrong
|
||||
// one rather than merely reading nothing.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";
|
||||
|
||||
GLint minLayer = -1;
|
||||
GLint numLayers = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLayer, kChosenLayer);
|
||||
EXPECT_EQ(numLayers, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image sliced = ReadFbo(viewFbo, kSize, kSize);
|
||||
const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
|
||||
static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
|
||||
ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
|
||||
"a single-layer 2D view of an array must address the layer it named");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Writing THROUGH a layer-sliced view. The read direction is covered above; this is the
|
||||
// write direction, and it is the one that can corrupt the parent rather than merely
|
||||
// return the wrong pixels - a view whose texel path forgot its layer origin writes over
|
||||
// the parent's layer 0 while the application believes it addressed layer minLayer.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, WritingThroughALayerSlicedViewLandsOnItsOwnLayers) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kViewMinLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const auto layerFill = [](int layer) {
|
||||
return Rgba8{static_cast<std::uint8_t>(10 + layer * 20),
|
||||
static_cast<std::uint8_t>(200 - layer * 20), 30, 255};
|
||||
};
|
||||
// Seeded by CPU sub-image rather than by rendering, deliberately: this scenario is
|
||||
// about the view's LAYER ORIGIN, and seeding through the GPU would additionally
|
||||
// depend on a CPU sub-image reaching a layer whose content the GPU wrote - which
|
||||
// DirectVulkan does not currently do even for a plain array texture (no view
|
||||
// involved), and which would make a failure here unattributable.
|
||||
const auto uploadLayer = [&](GLuint texture, int layer, Rgba8 colour) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = colour.r;
|
||||
texels[i + 1] = colour.g;
|
||||
texels[i + 2] = colour.b;
|
||||
texels[i + 3] = colour.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
};
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
uploadLayer(storage, layer, layerFill(layer));
|
||||
}
|
||||
const GLuint fbo = MakeFbo();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the layers raised an error";
|
||||
|
||||
// A two-layer window starting at layer 2, so a lost offset lands on layer 0 - which
|
||||
// the assertions below would see as an untouched layer that moved.
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, kViewMinLayer, 2);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Write the view's OWN layer 0, i.e. the storage's layer 2.
|
||||
constexpr Rgba8 kPainted{255, 0, 255, 255};
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = kPainted.r;
|
||||
texels[i + 1] = kPainted.g;
|
||||
texels[i + 2] = kPainted.b;
|
||||
texels[i + 3] = kPainted.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, view);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "writing through the view raised an error";
|
||||
|
||||
// POSITIVE CONTROL, through the parent's own name and into a layer outside the view's
|
||||
// window. It makes the assertions below able to tell "the view lost its layer origin"
|
||||
// from "a CPU sub-image into this array does not reach the GPU at all", which is a
|
||||
// different question and not one a texture view can answer.
|
||||
constexpr Rgba8 kControl{0, 0, 255, 255};
|
||||
std::vector<std::uint8_t> controlTexels(texels.size());
|
||||
for (std::size_t i = 0; i < controlTexels.size(); i += 4) {
|
||||
controlTexels[i + 0] = kControl.r;
|
||||
controlTexels[i + 1] = kControl.g;
|
||||
controlTexels[i + 2] = kControl.b;
|
||||
controlTexels[i + 3] = kControl.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
controlTexels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the control write raised an error";
|
||||
|
||||
// Read every layer of the PARENT back: only the one the view's layer 0 maps to may
|
||||
// have changed.
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
const Image image = ReadPixels(kSize, kSize);
|
||||
Rgba8 expected = layerFill(layer);
|
||||
const char* what = "a layer outside the view's window must not have been written";
|
||||
if (layer == kViewMinLayer) {
|
||||
expected = kPainted;
|
||||
what = "the view's layer 0 must be the storage layer it named";
|
||||
} else if (layer == 1) {
|
||||
expected = kControl;
|
||||
what = "control: a sub-image written through the PARENT must reach its layer";
|
||||
}
|
||||
ExpectRegion(image, 0, kSize - 1, 0, kSize - 1, expected, 2, what);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Views of views compose; the composed view still reaches the ROOT storage.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLevels = 3;
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
for (int level = 0; level < kLevels; ++level) {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
|
||||
glViewport(0, 0, kSize >> level, kSize >> level);
|
||||
glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
|
||||
// root's level 2. GL 4.6 core 8.18 makes the offsets add.
|
||||
const GLuint firstView = MakeTexture();
|
||||
glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
const GLuint secondView = MakeTexture();
|
||||
glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, secondView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
|
||||
ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
|
||||
"the composed view must land on the root's level 2");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// GL name-deletion semantics: the storage outlives the original's NAME.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The NAME goes; the storage may not, because a view still references it
|
||||
// (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
|
||||
glDeleteTextures(1, &storage);
|
||||
EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Sample the view through a shader, so the answer comes from a live descriptor rather
|
||||
// than from an attachment the frontend might have kept alive by other means.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
|
||||
glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
|
||||
ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
|
||||
"the view must still reach its storage after the original's name was deleted");
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,506 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundImageDescriptorScenario.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 - A PROGRAM DECLARES AN IMAGE-BACKED RESOURCE AND THE APPLICATION BINDS NOTHING.
|
||||
//
|
||||
// The sibling of GuiBatchScenario's MeshesBlockLeftUnbound, one descriptor kind further out.
|
||||
// That one pinned an unbound shader storage BLOCK; the same "nothing is bound, so lose the
|
||||
// whole draw" shape survived in the three image-backed kinds:
|
||||
//
|
||||
// * `samplerBuffer` - a texture unit with no buffer texture on it, and a buffer texture with
|
||||
// no GL buffer attached to it. Both make the sampler INCOMPLETE (GL 4.6
|
||||
// core 8.9, 8.24), and sampling an incomplete texture returns undefined
|
||||
// VALUES. It is not an error and it is not a lost draw.
|
||||
// * `imageBuffer` - an image unit with nothing on it. GL 4.6 core 8.26 is explicit: loads
|
||||
// return zero and stores are discarded.
|
||||
// * `image2D` - the same rule, through a VkImageView rather than a VkBufferView.
|
||||
//
|
||||
// Vulkan has no such thing as an unwritten descriptor, so DirectVulkan's descriptor resolution
|
||||
// used to answer "no valid descriptor" and both SetupDraw and DispatchCompute skip everything on
|
||||
// that answer - the draw or dispatch simply never happened, silently. Every test below asserts
|
||||
// on the OTHER work in the same shader: the pixels the fragment stage painted, or the buffer the
|
||||
// dispatch filled. All of it is unrelated to the unbound resource and all of it disappeared.
|
||||
//
|
||||
// The unbound resource is STATICALLY USED in every case, because an unreferenced one is
|
||||
// optimised out before it ever reaches a descriptor and would prove nothing. Where the use is a
|
||||
// read it sits behind a uniform-controlled branch that is false at runtime - the descriptor is
|
||||
// declared and must be written, but no undefined value reaches an assertion. Where it is a write
|
||||
// (the `writeonly` cases, which is how the real workloads spell it) it is unconditional: GL says
|
||||
// the store is discarded, so there is nothing to guard against.
|
||||
//
|
||||
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound unit to the GLES driver,
|
||||
// which does what GL says, so it is the control - every test here must stay green on both.
|
||||
|
||||
#include <cstdint>
|
||||
#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 kFboSize = 32;
|
||||
constexpr int kElements = 4;
|
||||
|
||||
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
|
||||
// vertex fetch can be confused with the descriptor question under test.
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The assertion in every draw case: opaque green everywhere. The unbound resource
|
||||
// contributes nothing to it - u_readUnbound is 0, so the fetch never runs - but the
|
||||
// descriptor for it still has to exist, which is the point.
|
||||
constexpr const char* kSamplerBufferFragmentSource = R"(#version 430 core
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSamplerBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
uint value = index + 1u;
|
||||
if (u_readUnbound != 0) {
|
||||
value += uint(texelFetch(u_unbound, 0).r);
|
||||
}
|
||||
g_data[index] = value;
|
||||
}
|
||||
)";
|
||||
|
||||
// writeonly, and the store is unconditional: this is how AcceleratedRendering and the
|
||||
// conformance cases spell an image the shader only produces into. GL discards the store
|
||||
// when the unit is empty; nothing here reads it back.
|
||||
constexpr const char* kImageBufferFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, 0, uvec4(7u));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImageBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, int(index), uvec4(7u));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
|
||||
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
|
||||
// level and no mip chain - GL calls the second one incomplete for the initial
|
||||
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
|
||||
// texture the backend then fails to back.
|
||||
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texture(u_unbound, vec2(0.0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
|
||||
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
|
||||
// unused one at an empty unit, so whichever of the two is unused has to have a
|
||||
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
|
||||
// cannot stand in for it.
|
||||
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform sampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, ivec2(0), 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
||||
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
||||
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
||||
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
||||
// been created.
|
||||
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform usampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform isampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, ivec2(0, 0), vec4(1.0));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// No layout format at all, which GLSL 4.20 allows for a write-only image. The reflection
|
||||
// then carries NO format for the binding, so the placeholder descriptor can only be
|
||||
// constrained by the declaration's numeric class - a different route through the fix than
|
||||
// every typed case above.
|
||||
constexpr const char* kFormatlessImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundImageDescriptorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenBuffers(1, &m_storage);
|
||||
// The harness shares one context across every scenario in the process, so an
|
||||
// earlier one may well have left a texture on unit 0 or an image on unit 0. The
|
||||
// whole subject here is that nothing is bound, so say so rather than assume it.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_storage != 0) glDeleteBuffers(1, &m_storage);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
// Each case needs exactly one kind of opaque uniform in one stage, and a host with
|
||||
// none of that kind there would report a failure that is about the host, not the fix.
|
||||
// Asked for by the limit that governs the kind under test and no other: a guard that
|
||||
// over-asks turns into a silent skip of the very thing the case exists for.
|
||||
static bool LimitIsAtLeastOne(GLenum limit) {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(limit, &value);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return value >= 1;
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the 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[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Fills a four-element SSBO with 1..4 while the unbound resource is declared and
|
||||
// statically used. Zeros everywhere mean the dispatch never ran.
|
||||
void ExpectDispatchStillRuns(const char* source, const char* what) {
|
||||
m_program = MakeComputeProgram(source);
|
||||
ASSERT_NE(m_program, 0u);
|
||||
|
||||
const std::vector<unsigned int> zeros(static_cast<std::size_t>(kElements), 0u);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
||||
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_storage);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting up the output buffer raised a GL error";
|
||||
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDispatchCompute(kElements, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error (" << what << ")";
|
||||
|
||||
std::vector<unsigned int> values(static_cast<std::size_t>(kElements), 0xDEADBEEFu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
||||
<< "element " << i << " came back as " << values[static_cast<std::size_t>(i)]
|
||||
<< "; zero everywhere means the whole dispatch was dropped over the unbound " << what;
|
||||
}
|
||||
}
|
||||
|
||||
// Paints the whole render target green while the unbound resource is declared and
|
||||
// statically used. A black target means the draw never happened.
|
||||
void ExpectDrawStillRuns(const char* fragmentSource, const char* what) {
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, fragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error (" << what << ")";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << "the readback came back empty";
|
||||
// Whole-region, not a centre pixel: the quad covers the target exactly, so
|
||||
// anything short of all of it is a failure worth naming.
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0,
|
||||
std::string("the quad drawn with an unbound ") + what))
|
||||
<< "an all-black target means the draw was dropped over the unbound " << what;
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_storage = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- uniform samplerBuffer (VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS)) {
|
||||
GTEST_SKIP() << "the compute stage has no texture image units";
|
||||
}
|
||||
ExpectDispatchStillRuns(kSamplerBufferComputeSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
// The other way a texel-buffer descriptor comes out empty: the unit HAS a buffer texture, but
|
||||
// no glTexBuffer ever attached a buffer object to it. GL calls that texture incomplete, which
|
||||
// is undefined data and not a lost draw - a separate site in the resolve from the one above,
|
||||
// and it used to return false too.
|
||||
TEST_F(UnboundImageDescriptorScenario, ABufferTextureWithNoAttachedBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
// Deliberately no glTexBuffer: the texture exists and is bound, and has no store.
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding an empty buffer texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "buffer texture with no attached buffer");
|
||||
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// ---- writeonly imageBuffer (VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImageBufferComputeSource, "imageBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImageBufferFragmentSource, "imageBuffer");
|
||||
}
|
||||
|
||||
// ---- writeonly image2D (VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) -------------------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImage2DComputeSource, "image2D");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
||||
}
|
||||
|
||||
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
|
||||
}
|
||||
|
||||
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
|
||||
// unit whose DEFAULT texture object has an image but no mip chain.
|
||||
//
|
||||
// DirectVulkan resolved such a binding twice, through two different predicates that
|
||||
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
|
||||
// pre-syncs and transitions every texture the draw will sample, asked only whether the
|
||||
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
|
||||
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
|
||||
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
|
||||
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
|
||||
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
|
||||
// a SIGSEGV inside the draw, not a degraded picture.
|
||||
//
|
||||
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
|
||||
// texture a base level, so the FIRST case in a process survived and every later one with an
|
||||
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
|
||||
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
|
||||
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
|
||||
// object bound there a FORMAT and a zero-sized level - which is what a bare
|
||||
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
|
||||
// state reset issues for every texture target. That combination is the whole point:
|
||||
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
|
||||
// and the texture stays in the sampled set;
|
||||
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
|
||||
// true and the descriptor holds the fallback instead;
|
||||
// * and it has no valid mip level, so the sync declines and hands back nullptr.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kFormatlessImage2DComputeSource, "format-less image2D");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,526 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.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 - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
|
||||
// QUALIFIER REMOVED.
|
||||
//
|
||||
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
|
||||
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
|
||||
// stages compile, the program links with an empty info log, the draw runs, and the consuming
|
||||
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
|
||||
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
|
||||
// from the blocks (and changing nothing else) makes the same program carry its payload. The
|
||||
// locations are not the application's in the first place: these shaders declare none, and
|
||||
// glslang's cross-stage IO resolver invents them.
|
||||
//
|
||||
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
|
||||
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
|
||||
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
|
||||
// location must still deliver its payload end to end. It is the assertion the affected device
|
||||
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
|
||||
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
|
||||
// cannot reproduce the defect at all.
|
||||
//
|
||||
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
|
||||
// entry, because llvmpipe carries a located block correctly and the driver POST would
|
||||
// therefore never turn the emulation on here. The SAME cases also run under the ambient
|
||||
// registrations with the emulation off, so both spellings of the interface are covered and a
|
||||
// regression in either shows up.
|
||||
//
|
||||
// Colour code, so a failure names its own cause:
|
||||
// green - the payload crossed all four stage boundaries, which is the pass.
|
||||
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
||||
// backend program was rejected and every draw became a no-op).
|
||||
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
||||
// failure is not about interface blocks.
|
||||
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
||||
// zeroed. That is what an interface whose two ends stopped matching looks like.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#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 {
|
||||
|
||||
// NOTHING in these five stages declares a location. Every location the emitted ESSL
|
||||
// carries is invented by the cross-stage resolver, which is exactly the shape the
|
||||
// affected driver mishandles and exactly what the strip removes.
|
||||
//
|
||||
// Two members per block, of different types, because an interface that is matched by
|
||||
// name and member sequence rather than by location has to agree on the sequence too -
|
||||
// a repair that silently reordered or dropped a member would still light up green with
|
||||
// one member in the block.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
out VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} vs_out;
|
||||
out float vs_tcs_alive;
|
||||
void main()
|
||||
{
|
||||
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
vs_out.tint = vec2(0.25, 0.5);
|
||||
vs_tcs_alive = 1.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
in VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_in[];
|
||||
in float vs_tcs_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_out[];
|
||||
out float tcs_tes_alive[];
|
||||
void main()
|
||||
{
|
||||
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
||||
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
|
||||
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Distinct block names, so this case is about the LOCATION and nothing else; the
|
||||
// one-name-in-both-directions shape is the case below.
|
||||
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// The 420pack shape: ONE name for the block this stage consumes and the block it
|
||||
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
|
||||
// the rename gives the two blocks one spelling per producing stage, the strip takes
|
||||
// their locations off, and the interfaces still have to meet.
|
||||
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// One geometry source per evaluation stage, because the block it consumes is named
|
||||
// after the block the evaluation stage produced.
|
||||
const char* const kDistinctGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kCollidingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// Green ONLY when both block members arrived: a repair that kept the first member and
|
||||
// lost the second would otherwise pass. Red when the plain varying is missing too, so
|
||||
// "the pipeline is broken" and "the block is broken" cannot be confused.
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
in GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} fs_in;
|
||||
in float gs_fs_alive;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
if (gs_fs_alive <= 0.5) {
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
|
||||
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
} else {
|
||||
fragColor = fs_in.payload;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
class UnlocatedIoBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!BackendHostsTessellationAndGeometry()) {
|
||||
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
||||
<< "carry a block through";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
|
||||
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
|
||||
// frontend constants, so a stack with no five-stage pipeline is recognised by
|
||||
// trying to build one, not by asking.
|
||||
static bool BackendHostsTessellationAndGeometry() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
||||
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
||||
geometrySource, kFragmentSource};
|
||||
|
||||
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
||||
bool ok = true;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
shaders[i] = glCreateShader(stages[i]);
|
||||
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
||||
glCompileShader(shaders[i]);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = InfoLog(shaders[i], true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) {
|
||||
if (shader != 0) glDeleteShader(shader);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (!linked) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
||||
// than something that could be mistaken for a zeroed payload.
|
||||
Rgba8 DrawAndReadCentre(GLuint program) const {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
return pixel;
|
||||
}
|
||||
|
||||
static bool IsGreen(const Rgba8& pixel) {
|
||||
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// The library log this process is writing, or an empty path when none was
|
||||
// configured. MOBILEGL_LOG_FILE_PATH is read at log-init, before anything this
|
||||
// fixture can reach, so the ctest entry sets it and this only reads it back.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// How many bytes the library log already holds. Everything this fixture asserts on
|
||||
// is searched from here forward, because the file is APPENDED to by every process
|
||||
// in the lane and a line left behind by an earlier one would otherwise satisfy the
|
||||
// assertion without this process having done anything at all.
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static GLenum FirstGLError() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
}
|
||||
return std::string(log.data());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
|
||||
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
|
||||
"member arrived wrong, i.e. the interface stopped matching)";
|
||||
}
|
||||
|
||||
// The two repairs together. The rename is what makes the evaluation stage's two
|
||||
// TcsData blocks one spelling per producing stage; the strip then takes the locations
|
||||
// off the names the rename just settled. Either one alone leaves a working program on
|
||||
// these machines, so this case is here to catch the two of them disagreeing.
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "an interface block name reused across the two directions of one stage is legal "
|
||||
"desktop GLSL, but the program did not build: "
|
||||
<< BuildLog();
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REPAIR SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above renders green on llvmpipe whether the blocks were stripped or not -
|
||||
// this machine carries a located block correctly - so those cases pin that the strip
|
||||
// does no HARM and can say nothing about whether it happened. That leaves the arming
|
||||
// itself untested, and the arming is where the cheap mistake lives: Loader.cpp maps
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS onto the capability INVERTED (forcing the
|
||||
// emulation on means declaring located blocks UNSUPPORTED), and a one-line swap of
|
||||
// those two arms would disable the device repair with every test here still green.
|
||||
//
|
||||
// So this case asserts a LIBRARY OBSERVABLE against the environment, the shape
|
||||
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration uses: the environment
|
||||
// says the emulation is pinned on, therefore the library must SAY it stripped
|
||||
// something. The observable is the latched MGLOG_I DirectGLES emits the first time the
|
||||
// pass fires (Managers.cpp); it is INFO rather than DEBUG precisely so that this
|
||||
// assertion is possible in the builds CI runs.
|
||||
//
|
||||
// Two things it deliberately does NOT do: it does not read MG_Config (on Android this
|
||||
// module links the shipping library, which exports nothing internal - the reason
|
||||
// ViewportArrayScenario's control moved to the environment), and it does not trust the
|
||||
// whole log file, only the bytes appended after this test started.
|
||||
TEST_F(UnlocatedIoBlockScenario, TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the emulation pinned ON for the whole process, which "
|
||||
"is what the UnlocatedIoBlocks. ctest entry does with "
|
||||
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1; with the variable unset the "
|
||||
"driver POST decides, and on this machine it decides the blocks are "
|
||||
"fine - so there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it stripped anything; the UnlocatedIoBlocks. ctest entry "
|
||||
"sets both";
|
||||
}
|
||||
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||
GTEST_SKIP() << "the strip is DirectGLES's; " << Gl().BackendName()
|
||||
<< " hands the module to the driver as SPIR-V, where Location is how "
|
||||
"interfaces are matched";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote. The latch means it is emitted at the FIRST stage of the
|
||||
// FIRST affected program, which is inside the build below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so nothing would arm the strip: " << BuildLog();
|
||||
}
|
||||
// Drawn as well as built, so a stack that defers its backend program to first use
|
||||
// still reaches the transpile this is asserting about.
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre)) << "the pinned-on lane did not even render correctly: " << centre;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("WITHOUT their layout(location) qualifier"), std::string::npos)
|
||||
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned ON, a five-stage program with four "
|
||||
"interface-block boundaries was built and drawn, and DirectGLES never reported "
|
||||
"stripping a single location. The emulation is not armed - check the override "
|
||||
"mapping in Loader.cpp (it is inverted on purpose) and the arming gate in "
|
||||
"Managers.cpp. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,276 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnwrittenPositionOutputScenario.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 - A SHADER REDECLARES gl_PerVertex AND NEVER WRITES gl_Position.
|
||||
//
|
||||
// Legal, ordinary GLSL, and until now a process kill on DirectVulkan. The chain, all of it
|
||||
// inside MobileGL's own SPIR-V plumbing:
|
||||
//
|
||||
// 1. glslang emits every DECLARED interface variable, used or not, and lists it on
|
||||
// OpEntryPoint. So `out gl_PerVertex { vec4 gl_Position; };` with no write still produces
|
||||
// the OpVariable, the OpMemberDecorate BuiltIn Position, and an interface slot.
|
||||
// 2. At link, ShaderCompiler::SanitizeAndOptimizeBinary runs AggressiveDCE(remove_outputs =
|
||||
// false) - which may never delete an Output - and then RemoveUnusedInterfaceVariables,
|
||||
// which rebuilds the interface list from the variables instructions actually reference.
|
||||
// The OpVariable and its BuiltIn decoration SURVIVE; the interface slot is DELISTED.
|
||||
// 3. At pipeline build, ProgramFactory picks the last pre-rasterisation stage and runs two
|
||||
// passes over it. GlToVulkanPositionFixPass finds the position target through the
|
||||
// surviving ANNOTATION and injects a load-modify-STORE through it. When gl_Position is in
|
||||
// the transform-feedback capture list, XfbCaptureDecoratePass::MirrorPositionForCapture
|
||||
// also injects an access chain and a LOAD through it.
|
||||
// 4. Either injection is a static use of a variable that is no longer on the entry point's
|
||||
// interface, which is invalid SPIR-V ("Interface variable id <N> is used by entry point
|
||||
// 'main' id <M>, but is not listed as an interface"). Mali r54 does not reject such a
|
||||
// module - it faults inside pipeline creation and takes the process down.
|
||||
//
|
||||
// Measured on a Mali-G1-Ultra as 216 KHR-GL44/45/46.tessellation_shader.tessellation_control_
|
||||
// to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize_* crashes; the CTS's TES
|
||||
// there is exactly the shape below. It is not tessellation-specific and not XFB-specific: a
|
||||
// vertex shader is enough, which is what these cases use.
|
||||
//
|
||||
// Every test captures a USER varying through transform feedback under GL_RASTERIZER_DISCARD.
|
||||
// Position is undefined in the first two by construction, so it is never asserted on - what is
|
||||
// asserted is that the capture came back at all, which it can only do if the driver accepted
|
||||
// the module and built a pipeline.
|
||||
|
||||
#include <cstddef>
|
||||
#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 std::size_t kCaptureFloats = 4;
|
||||
constexpr GLsizeiptr kCaptureBytes = static_cast<GLsizeiptr>(kCaptureFloats * sizeof(float));
|
||||
|
||||
// The defect's shape: gl_PerVertex redeclared, gl_Position never assigned.
|
||||
constexpr const char* kUnwrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The control that isolates the redeclaration: identical but for the one assignment.
|
||||
// This one keeps its interface slot through the sanitize chain, so both injections were
|
||||
// always legal on it - it must stay working.
|
||||
constexpr const char* kWrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The second control, and the one the CTS calls data_pass_through: no gl_PerVertex
|
||||
// redeclaration at all, so there is no Position annotation for the passes to find and
|
||||
// nothing to delist. It was never affected and proves the crash needs the redeclaration.
|
||||
constexpr const char* kNoPositionBlockVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
GLuint CompileVertexShader(const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// `captureNames` is what goes to glTransformFeedbackVaryings. Passing gl_Position in it
|
||||
// is what puts MirrorPositionForCapture on the path.
|
||||
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::string* log) {
|
||||
const GLuint vertexShader = CompileVertexShader(vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(captureNames.size()), captureNames.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
class UnwrittenPositionOutputScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const float vertex[kCaptureFloats] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, vertex, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// Links `vertexSource` with `captureNames`, runs one captured point, and checks that
|
||||
// the USER varying came back. `captureStride` is how many floats one captured vertex
|
||||
// occupies, so the user varying can be read out from behind a captured gl_Position.
|
||||
void ExpectUserVaryingIsCaptured(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::size_t captureStride, std::size_t userVaryingOffset,
|
||||
const char* what) {
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(vertexSource, captureNames, &log);
|
||||
ASSERT_NE(program, 0u) << what << ": the capture program failed to build: " << log;
|
||||
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(captureStride * sizeof(float));
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
// Pre-fill with a value the shader cannot produce, so "captured nothing" is
|
||||
// distinguishable from "captured the wrong thing".
|
||||
const std::vector<float> poison(captureStride, -1.0f);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, captureBytes, poison.data(), GL_DYNAMIC_READ);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << what << ": setting up the capture buffer raised a GL error";
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the captured draw raised a GL error";
|
||||
|
||||
std::vector<float> readback(captureStride, -2.0f);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, readback.data());
|
||||
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
|
||||
EXPECT_FLOAT_EQ(readback[userVaryingOffset + i], static_cast<float>(i + 1))
|
||||
<< what << ": captured float " << i << " came back as "
|
||||
<< readback[userVaryingOffset + i]
|
||||
<< "; the pre-fill value means the draw never produced a vertex, which is what an "
|
||||
"invalid shader module looks like from out here";
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The clip fixup's half: PositionZRemap is on for every draw, so the fixup runs on this
|
||||
// program and used to inject a store through the delisted block.
|
||||
TEST_F(UnwrittenPositionOutputScenario, ARedeclaredButUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared, never written");
|
||||
}
|
||||
|
||||
// The XFB half: capturing gl_Position adds an access chain and a LOAD through the same
|
||||
// delisted block, which the interface rule covers exactly as it covers the store. Position
|
||||
// itself is undefined here - only the user varying behind it is asserted.
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAnUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// DirectVulkan only, and not because the defect was backend-specific in principle - the
|
||||
// injection this pins lives in DirectVulkan's ProgramFactory, and DirectGLES cannot
|
||||
// reach the case at all: capturing gl_Position BY NAME off a shader that never writes it
|
||||
// comes back empty there, because the ESSL the transpiler emits has no such output for
|
||||
// the capture list to name. That is a known, separate DirectGLES gap (the same one that
|
||||
// blocks gl_Position/gl_PointSize capture in the tessellation capture segment), tracked
|
||||
// outside this scenario; asserting it here would only re-report it.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "capturing an unwritten gl_Position by name is a separate, known "
|
||||
<< "DirectGLES gap; this case pins the DirectVulkan injection";
|
||||
}
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing an unwritten gl_Position");
|
||||
}
|
||||
|
||||
// Control: the same shader with the one assignment restored. Its block is never delisted,
|
||||
// so it exercises the path the fixup is actually for and must keep working.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AWrittenRedeclaredPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared and written");
|
||||
}
|
||||
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAWrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing a written gl_Position");
|
||||
}
|
||||
|
||||
// Control: no gl_PerVertex redeclaration, so no Position annotation and nothing to delist.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AShaderWithNoPositionBlockStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kNoPositionBlockVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"no gl_PerVertex block");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -55,10 +55,6 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
|
||||
// to know which backend it is running on.
|
||||
#include <Config.h>
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -529,7 +525,8 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
//
|
||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
||||
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
|
||||
// the SAME program in a process started with MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
|
||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
||||
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
|
||||
@@ -544,25 +541,31 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
"gl_ViewportIndex natively and ignores it";
|
||||
}
|
||||
|
||||
// The feature table is a process-global and this fixture shares its context with every
|
||||
// other scenario in the process, so the restore is not optional.
|
||||
struct ScopedEmulationOff {
|
||||
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
|
||||
MobileGL::MG_Config::Features.ViewportArrayEmulation =
|
||||
MobileGL::MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
|
||||
MobileGL::MG_Config::QuirkOverride saved;
|
||||
};
|
||||
// The switch comes from the ENVIRONMENT, and this case runs only in a process that
|
||||
// was started with it off. It used to write MG_Config::Features directly, which is
|
||||
// not available to it any more: on Android this module links the shipping
|
||||
// libMobileGL.so - so that the on-device run validates the real artifact - and that
|
||||
// library exports no such symbol. The process-wide variable is also the more honest
|
||||
// spelling of the control, since it is the one a developer chasing this failure
|
||||
// would actually set. CMakeLists.txt registers the NoViewportArrayEmulation. ctest
|
||||
// entry for it, so the control still runs in every ctest run; anywhere else - the
|
||||
// ambient ctest entries, or the binary run straight from a device shell - the
|
||||
// emulation is on and this case skips.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the "
|
||||
"whole process; the NoViewportArrayEmulation. ctest entry runs it with "
|
||||
"MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
}
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
|
||||
GLuint unroutedProgram = 0;
|
||||
{
|
||||
const ScopedEmulationOff scopedEmulationOff;
|
||||
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
|
||||
// that carries this flag, so reusing m_program would just replay the routed build.
|
||||
// A program of its own rather than the fixture's, even though in this process
|
||||
// the fixture's was built unrouted too: the emitted ESSL is decided at link
|
||||
// time and memoized on a key that carries this flag, and building it here keeps
|
||||
// what this case measures independent of when SetUp happened to link.
|
||||
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
||||
glUseProgram(unroutedProgram);
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.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 CAPTURE MUST STILL RECORD WHEN IT IS NOT THE FIRST ONE IN THE PROCESS,
|
||||
// AND THE CAPTURE STAGE MAY BE ANY OF THE FOUR THAT CAN BE THE LAST ONE.
|
||||
//
|
||||
// The conformance suite exposed a whole family of transform feedback failures that no
|
||||
// existing scenario could reproduce, because every one of them ran ONE capture, from a
|
||||
// VERTEX stage, in a freshly initialised process. What the suite actually does is
|
||||
// different in three ways at once, and each of them turned out to matter:
|
||||
//
|
||||
// * it runs case after case in ONE GL context, resetting state between them - and the
|
||||
// reset is not a fresh context. Its transform feedback part
|
||||
// (framework/opengl/gluStateReset.cpp resetStateGLCore) unbinds the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER and then clears every indexed capture point from 0 to
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, which permanently raises MobileGL's
|
||||
// touched-binding-point high-water mark. Every later capture that uses fewer points
|
||||
// than that - i.e. every INTERLEAVED_ATTRIBS capture - then had the unused tail
|
||||
// re-cleared on the driver immediately before glBeginTransformFeedback.
|
||||
// ReplayDeqpStateReset below is that reset, reduced to the calls that touch capture
|
||||
// state, so a defect that only appears from the second capture onwards is reachable
|
||||
// here instead of only on a device.
|
||||
//
|
||||
// * the capture stage is frequently a GEOMETRY or a TESSELLATION EVALUATION shader,
|
||||
// never a plain vertex shader. The tree had zero coverage for either: none of the
|
||||
// Xfb* scenarios mentioned tessellation and neither TessellationDrawModeScenario nor
|
||||
// GeometryDrawModeScenario mentioned transform feedback.
|
||||
//
|
||||
// * the capture program frequently has NO FRAGMENT STAGE at all, because it draws
|
||||
// under GL_RASTERIZER_DISCARD and never rasterises anything. That is legal in
|
||||
// desktop GL and the shape most "use transform feedback as a readback channel"
|
||||
// tests are built on.
|
||||
//
|
||||
// Every case here asserts the captured BYTES, never just the absence of a GL error: the
|
||||
// failure this guards against writes nothing and raises nothing, so a buffer that kept
|
||||
// its poison is the only thing that distinguishes it from success.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure ("the capture never reached these bytes") instead of looking
|
||||
// like an ordinary numeric mismatch.
|
||||
constexpr int kPoison = -987654;
|
||||
|
||||
const char* const kPassthroughVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in int vs_in_value;
|
||||
flat out int vs_out_value;
|
||||
void main()
|
||||
{
|
||||
vs_out_value = vs_in_value;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The primitive_counter shape: one flat int per emitted vertex, several vertices
|
||||
// per input primitive, so the capture is geometry-AMPLIFIED and the CPU-side
|
||||
// primitive model cannot predict its length.
|
||||
const char* const kPointAmplifyingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 2) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
gs_out_value = vs_out_value[0];
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Adjacency input. Only a geometry stage can consume it, and CountPrimitivesForDraw
|
||||
// used to answer 0 for every adjacency mode, which silently excluded the whole draw
|
||||
// from the capture accounting.
|
||||
const char* const kAdjacencyGeometrySource = R"(#version 420 core
|
||||
layout(lines_adjacency) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
gs_out_value = vs_out_value[1];
|
||||
gl_Position = gl_in[1].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
patch out int tcs_out_value;
|
||||
void main()
|
||||
{
|
||||
tcs_out_value = vs_out_value[0];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
patch in int tcs_out_value;
|
||||
flat out int tes_out_value;
|
||||
void main()
|
||||
{
|
||||
tes_out_value = tcs_out_value;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
flat in int gs_out_value;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(float(gs_out_value), 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class XfbRepeatedCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const int values[kInputVertices] = {10, 11, 12, 13};
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(values), values, GL_STATIC_DRAW);
|
||||
glVertexAttribIPointer(0, 1, GL_INT, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vbo = 0;
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static constexpr int kInputVertices = 4;
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 2;
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
// The transform-feedback-relevant half of deqp's resetStateGLCore, in its order.
|
||||
// It runs between EVERY pair of conformance cases, and running one capture
|
||||
// through it is the difference between "the first capture in the process" and
|
||||
// every other one.
|
||||
static void ReplayDeqpStateReset() {
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
GLint maxSeparateAttribs = 0;
|
||||
glGetIntegerv(GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSeparateAttribs);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
for (GLint index = 0; index < maxSeparateAttribs; ++index) {
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(index), 0);
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const char* varying) {
|
||||
return BuildCaptureProgram(stages, std::vector<const char*>{varying});
|
||||
}
|
||||
|
||||
// Builds a capture program out of `stages` capturing `varyings` interleaved.
|
||||
// Returns 0 and fills m_buildLog on failure.
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span. `captureMode` is the transform feedback primitive mode,
|
||||
// `drawMode`/`count` the draw. Returns the capture buffer's contents.
|
||||
std::vector<int> RunCaptureSpan(GLuint program, GLenum captureMode, GLenum drawMode, GLsizei count,
|
||||
std::size_t capturedInts) {
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// The capture point is the ONLY thing bound; the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER binding comes along for the ride, exactly as
|
||||
// the conformance tests rely on (GL 4.6 core 6.1.1).
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(drawMode, 0, count);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<int> readback(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
glUseProgram(0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedNothing(const std::vector<int>& data) {
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != kPoison) {
|
||||
return ::testing::AssertionFailure() << "component " << i << " is " << data[i];
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedIs(const std::vector<int>& data,
|
||||
const std::vector<int>& expected) {
|
||||
if (data.size() != expected.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "captured " << data.size() << " value(s), expected " << expected.size();
|
||||
}
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != expected[i]) {
|
||||
::testing::AssertionResult failure = ::testing::AssertionFailure();
|
||||
failure << "component " << i << " is " << data[i] << ", expected " << expected[i];
|
||||
if (data[i] == kPoison) {
|
||||
failure << " (the capture never reached these bytes)";
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// THE REGRESSION GUARD FOR THE WHOLE FAMILY. Two geometry-stage captures in one
|
||||
// process with the conformance suite's own state reset between them; the assertion
|
||||
// that matters is on the SECOND one, which is the one every device run failed while
|
||||
// whichever body happened to land first in its process passed.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASecondGeometryCaptureAfterADeqpStateResetStillRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
// Two vertices emitted per input point, so the capture is amplified beyond what
|
||||
// the CPU primitive model can predict from the draw alone.
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
|
||||
for (int capture = 0; capture < 3; ++capture) {
|
||||
// A fresh program per capture, because that is what a fresh conformance case
|
||||
// builds - and it is what makes the driver recycle program and buffer names.
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture " << capture << " program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected))
|
||||
<< "capture " << capture << " of 3 in this process"
|
||||
<< (capture == 0 ? "" : " (every earlier one was followed by a deqp-shaped state reset)");
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "capture " << capture;
|
||||
|
||||
glDeleteProgram(program);
|
||||
m_programs.pop_back();
|
||||
ReplayDeqpStateReset();
|
||||
glBindVertexArray(m_vao);
|
||||
}
|
||||
}
|
||||
|
||||
// The tessellation half, which had no coverage anywhere in the tree: a capture taken
|
||||
// from a GL_PATCHES draw, whose last vertex-processing stage is the evaluation shader
|
||||
// and whose record count only the tessellator knows.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAPatchesDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
|
||||
// One input patch of one vertex, all levels at 1: the tessellator emits exactly
|
||||
// one triangle, so three captured vertices all carrying the first input value.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
|
||||
"tes_out_value");
|
||||
ASSERT_NE(program, 0u) << "patch capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 10};
|
||||
const std::vector<int> captured = RunCaptureSpan(program, GL_TRIANGLES, GL_PATCHES, 1, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A capture program with NO FRAGMENT STAGE, drawn under GL_RASTERIZER_DISCARD. Legal
|
||||
// in desktop GL, and the shape most transform-feedback-as-readback tests use; the
|
||||
// program above only differs from it by the fragment shader, so a failure here is
|
||||
// specifically about the missing stage.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAFragmentlessProgramRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "fragmentless capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An ADJACENCY draw feeding the capture. CountPrimitivesForDraw answered 0 for all
|
||||
// four adjacency modes, which made the transform feedback accounting skip the draw
|
||||
// entirely - so neither the captured-vertex counter nor the geometry-capture-draw
|
||||
// flag moved, and anything downstream of either was working from "nothing happened".
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAnAdjacencyDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kAdjacencyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "adjacency capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line primitive; the shader emits
|
||||
// the second vertex of the four, which is the line's first real endpoint.
|
||||
const std::vector<int> expected = {11};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An adjacency draw with NO geometry stage. GL 4.6 core table 13.1 admits
|
||||
// GL_LINES_ADJACENCY and GL_LINE_STRIP_ADJACENCY under capture mode GL_LINES (and the
|
||||
// triangle pair under GL_TRIANGLES): without a geometry shader the adjacent vertices
|
||||
// are ignored and the primitive assembled is a plain line, so the combination is legal
|
||||
// and must capture. MobileGL's active-capture primitive-mode table listed only the
|
||||
// non-adjacency modes, so this raised GL_INVALID_OPERATION and dropped the draw
|
||||
// entirely - the buffer kept its pre-draw bytes and the application saw an error the
|
||||
// spec does not allow. Distinct from ACaptureFromAnAdjacencyDrawRecords above, which
|
||||
// HAS a geometry stage and therefore bypasses that table completely.
|
||||
TEST_F(XfbRepeatedCaptureScenario, AVertexOnlyAdjacencyCaptureRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(program, 0u) << "vertex-only capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line whose real endpoints are the
|
||||
// middle pair, so the capture is those two vertices in order.
|
||||
const std::vector<int> expected = {11, 12};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_LINES, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
|
||||
// THE GUARD FOR THE DEFECT ITSELF, and it is backend-independent: the frontend
|
||||
// validator must not reject the combination. It used to record
|
||||
// GL_INVALID_OPERATION and return before the draw was ever issued.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR)
|
||||
<< "a capture-mode/draw-mode pair GL 4.6 core table 13.1 admits must raise no error";
|
||||
|
||||
// Whether the capture then RECORDS is a backend question, and the two answer it
|
||||
// differently. ES 3.2 (10.1) supports the adjacency primitive types only for a
|
||||
// pipeline with a geometry shader, so DirectGLES has nothing to forward this draw
|
||||
// to; desktop GL and Vulkan both assemble the plain line and capture it. Asserting
|
||||
// the data unconditionally would be asserting that DirectGLES emulates a whole ES
|
||||
// restriction away, which is a separate piece of work and not what this guards.
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "DirectGLES cannot forward a geometry-shader-less adjacency draw: ES 3.2 10.1 "
|
||||
"supports the adjacency primitive types only with a geometry stage. The frontend "
|
||||
"no longer rejects the draw (checked above), which is the defect this covers.";
|
||||
}
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
}
|
||||
|
||||
// A CAPTURE MUST NEVER LAND IN A BUFFER THE APPLICATION DID NOT BIND FOR IT.
|
||||
//
|
||||
// A capture list may legally begin with gl_NextBuffer, which leaves capture buffer 0
|
||||
// with stride 0 and nothing to capture - so glBeginTransformFeedback does not require a
|
||||
// buffer at point 0 and the application binds only point 1. The driver-side program is
|
||||
// a single-buffer interleaved capture (the pseudo-varyings are consumed at link time),
|
||||
// so it writes capture point 0, and MobileGL redirects that into scratch storage and
|
||||
// scatters the records afterwards.
|
||||
//
|
||||
// Two ways that went wrong, both fixed here: the scratch was sized by reading each
|
||||
// target's stride at its POSITION in a list that skips unbound buffers, which for this
|
||||
// layout read stride 0 for everything and produced a zero capacity; and when the
|
||||
// scratch then failed to bind, the span opened anyway onto whatever capture point 0
|
||||
// still held from an earlier capture in the process - silently overwriting an unrelated
|
||||
// application buffer. The first span below exists purely to leave such a binding behind.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureListBeginningWithGlNextBufferSparesTheEarlierBuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::size_t capturedInts = 4;
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(capturedInts * sizeof(int));
|
||||
|
||||
// Span A: an ordinary capture, so capture point 0 is left holding bufferA.
|
||||
const GLuint programA =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(programA, 0u) << "plain capture program failed to build: " << m_buildLog;
|
||||
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint bufferA = 0;
|
||||
glGenBuffers(1, &bufferA);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, bufferA);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(programA);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> afterA(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, afterA.data());
|
||||
const std::vector<int> spanAExpected = {10, 11, 12, 13};
|
||||
ASSERT_TRUE(CapturedIs(afterA, spanAExpected)) << "the setup span itself did not capture";
|
||||
|
||||
// Span B: gl_NextBuffer first, so buffer 0 captures nothing and only point 1 is bound.
|
||||
const GLuint programB = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}},
|
||||
{"gl_NextBuffer", "vs_out_value"});
|
||||
if (programB == 0) {
|
||||
GTEST_SKIP() << "gl_NextBuffer capture lists are not linkable on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "): " << m_buildLog;
|
||||
}
|
||||
|
||||
GLuint bufferB = 0;
|
||||
glGenBuffers(1, &bufferB);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferB);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// Point 0 released, point 1 is the only destination this capture asks for.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, bufferB);
|
||||
|
||||
glUseProgram(programB);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
// THE ASSERTION THAT MATTERS: bufferA was not a destination of this capture, so it
|
||||
// must still read exactly what span A left in it. A failure here is the corruption.
|
||||
std::vector<int> bufferAAfterB(capturedInts, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, captureBytes, bufferAAfterB.data());
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
EXPECT_TRUE(CapturedIs(bufferAAfterB, spanAExpected))
|
||||
<< "the gl_NextBuffer capture wrote into the buffer the PREVIOUS span had bound";
|
||||
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and, where the backend places this layout at all, the buffer it WAS asked to
|
||||
// write gets the records. That placement is the DirectGLES scatter path, whose
|
||||
// scratch sizing used to read each target's stride at its POSITION in a list that
|
||||
// skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0
|
||||
// for every target and sized the scratch at zero. DirectVulkan does not implement a
|
||||
// leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a
|
||||
// pre-existing gap of its own, and the assertion above - that it corrupts nothing
|
||||
// while declining - is what matters for it.
|
||||
const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan";
|
||||
if (backendPlacesLeadingNextBuffer) {
|
||||
std::vector<int> bufferBAfter(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data());
|
||||
EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected));
|
||||
}
|
||||
|
||||
// Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer
|
||||
// this test deleted would follow the process into the next scenario.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0);
|
||||
glDeleteBuffers(1, &bufferA);
|
||||
glDeleteBuffers(1, &bufferB);
|
||||
|
||||
if (!backendPlacesLeadingNextBuffer) {
|
||||
GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it "
|
||||
"captures nothing, which the no-corruption assertion above has already covered.";
|
||||
}
|
||||
}
|
||||
|
||||
// The control for all of the above: a span that never draws must leave the capture
|
||||
// buffer alone. Without it "the buffer kept its poison" could be read as the correct
|
||||
// outcome of some path rather than as the bug, and the tightened early returns in
|
||||
// StartPendingTransformFeedback have to keep this legal case legal.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASpanThatNeverDrawsLeavesTheCaptureBufferAlone) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::size_t capturedInts = 8;
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glEndTransformFeedback();
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> readback(capturedInts, 0);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
EXPECT_TRUE(CapturedNothing(readback));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "BufferObject.h"
|
||||
|
||||
#include <Config.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
@@ -65,6 +67,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
|
||||
if (size == 0) {
|
||||
// An empty write moves the serial and nothing else, exactly as NotifySubData
|
||||
// and NotifyFlushMappedRange do: it wrote no byte, so it must not promote an
|
||||
// undefined store to "has content" - that would cost the next orphaning
|
||||
// respecification a full-size upload of bytes the application never wrote.
|
||||
++m_changeSerial;
|
||||
return;
|
||||
}
|
||||
m_hasDefinedContent = true;
|
||||
if (m_resource.IsGpuResident()) {
|
||||
// The write already landed in coherent GPU memory; the backend has no separate
|
||||
@@ -109,7 +119,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::Respecify(SizeT size, const void* data) {
|
||||
ReleaseMemory();
|
||||
// The store a live mapping wrote into is about to be replaced, so landing those
|
||||
// bytes into it would copy a whole mapped range (an adopted arena's map is the
|
||||
// arena) into storage the next line hands back.
|
||||
ReleaseMemory(false);
|
||||
RedefineStorage(size);
|
||||
if (data && size > 0) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
@@ -118,8 +131,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// record that so backends skip uploading the stale shadow bytes.
|
||||
m_hasDefinedContent = (data != nullptr) || size == 0;
|
||||
m_isImmutableStorage = false;
|
||||
m_storageFlags = 0;
|
||||
// GL 4.6 core 6.2 defines glBufferData as glBufferStorage with
|
||||
// DYNAMIC_STORAGE_BIT | MAP_READ_BIT | MAP_WRITE_BIT, so GL_BUFFER_STORAGE_FLAGS has to
|
||||
// report those three afterwards. Reporting 0 - the value that belongs to a buffer whose
|
||||
// store has never been specified - told an application that a perfectly writable
|
||||
// glBufferData buffer accepted neither glBufferSubData nor a map. Only the IMMUTABLE flag
|
||||
// distinguishes the two cases, and it is cleared just above.
|
||||
m_storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT;
|
||||
NotifyRespecify();
|
||||
TryAdoptLargeStorage();
|
||||
}
|
||||
|
||||
void BufferObject::Resize(SizeT size) {
|
||||
@@ -127,7 +147,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
|
||||
ReleaseMemory();
|
||||
// Same as Respecify: the bytes a live mapping staged have nowhere to land, the
|
||||
// store they belong to is being replaced.
|
||||
ReleaseMemory(false);
|
||||
RedefineStorage(size);
|
||||
if (data) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
@@ -138,6 +160,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_isImmutableStorage = true;
|
||||
m_storageFlags = storageFlags;
|
||||
NotifyRespecify();
|
||||
TryAdoptLargeStorage();
|
||||
}
|
||||
|
||||
// Back a LARGE store with the backend's persistently+coherently mapped GPU
|
||||
// storage the moment it is (re)defined, without waiting for the app to map it.
|
||||
// Minecraft 26.3 streams chunk meshes into 128MB vertex arenas with plain
|
||||
// glNamedBufferSubData - the one write API that carries no synchronization
|
||||
// hint - and on Mali every route that hands the driver a write into a busy
|
||||
// MUTABLE store either parks the calling thread (glBufferSubData, and
|
||||
// glMapBufferRange even with GL_MAP_UNSYNCHRONIZED_BIT) or ghost-copies the
|
||||
// whole destination on a driver worker (staged glCopyBufferSubData, and a
|
||||
// range-invalidating map: ~167ms per touched arena, the recurring in-world
|
||||
// hiccup). An adopted coherent map is the one shape with NO per-write driver
|
||||
// call at all: every SubData lands as a plain memcpy into GPU-visible memory,
|
||||
// and the shadow copy is dropped (a 128MB arena stops costing 128MB of RAM).
|
||||
// Only attempted for stores the size of mesh arenas: small buffers keep the
|
||||
// shadow model whose draw-time flush already prices them correctly.
|
||||
void BufferObject::TryAdoptLargeStorage() {
|
||||
constexpr SizeT kLargeBufferAdoptBytes = 16u * 1024u * 1024u;
|
||||
if (MG_Config::Features.DisableLargeBufferAdoption) return;
|
||||
if (m_size < kLargeBufferAdoptBytes) return;
|
||||
if (m_resource.IsGpuResident()) return;
|
||||
if (m_isMapped) return;
|
||||
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
||||
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
}
|
||||
}
|
||||
|
||||
void BufferObject::UploadData(DataPtr data, SizeT atOffset) {
|
||||
@@ -154,24 +203,45 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_usage = usage;
|
||||
}
|
||||
|
||||
void BufferObject::ReleaseMemory() {
|
||||
void BufferObject::ReleaseMemory(Bool landStagedWrites) {
|
||||
if (!m_isMapped) return;
|
||||
|
||||
if (m_mappingAccess & BufferMappingAccessBit::Write) { // if we wrote to the buffer
|
||||
// A persistent GPU-resident map wrote straight into coherent GPU memory, so
|
||||
// there is nothing to copy back and no range to push down on unmap.
|
||||
if (!m_resource.IsGpuResident() &&
|
||||
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data() + m_stagingBias,
|
||||
m_mappedRange.end - m_mappedRange.start);
|
||||
if (landStagedWrites &&
|
||||
(m_mappingAccess & BufferMappingAccessBit::Write)) { // if we wrote to the buffer
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
||||
const SizeT mappedLength = m_mappedRange.end - m_mappedRange.start;
|
||||
if (m_resource.IsGpuResident()) {
|
||||
// A persistent map of an adopted store wrote straight into coherent
|
||||
// GPU memory: nothing to copy back, no range to push down. A
|
||||
// NON-persistent write map is a different thing: the application
|
||||
// wrote a staging copy (glMapBuffer and glMapBufferRange hand one out
|
||||
// regardless of where the store lives), and GL requires those bytes
|
||||
// to be visible to every later command the moment glUnmapBuffer
|
||||
// returns. Residency used to come only from a coherent persistent
|
||||
// map, which never has a staging copy, so the copy-back was simply
|
||||
// skipped for a resident store; residency now also comes from a
|
||||
// shader storage binding (EnsureGpuResidentStorage at draw time) and
|
||||
// from large-store adoption (TryAdoptLargeStorage), both of which an
|
||||
// application then re-initialises through an ordinary map/write/unmap.
|
||||
// Skipping the copy-back dropped every one of those writes. Land the
|
||||
// staged bytes through the same route glBufferSubData takes into an
|
||||
// adopted store - the backend's flush op is for stores it keeps a
|
||||
// separate copy of and must not run here.
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||
LandBytesIntoResidentStore(m_mappedRange.start,
|
||||
{m_stagingData.data() + m_stagingBias, mappedLength});
|
||||
}
|
||||
} else {
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data() + m_stagingBias,
|
||||
mappedLength);
|
||||
}
|
||||
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
||||
}
|
||||
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
||||
}
|
||||
|
||||
m_stagingData.clear();
|
||||
}
|
||||
|
||||
m_stagingData.clear();
|
||||
m_isMapped = false;
|
||||
m_mappingAccess = BufferMappingAccessBit::Null;
|
||||
m_mappedRange = {0, 0};
|
||||
@@ -191,8 +261,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
MOBILEGL_ASSERT(end <= m_mappedRange.end, "Flush range out of bounds: mappedRange.end (%zu) < end (%zu)",
|
||||
m_mappedRange.end, end);
|
||||
|
||||
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
|
||||
// the staged bytes must be copied into the shadow before the backend reads them.
|
||||
// A FLUSH_EXPLICIT map can sit on an adopted store: the map itself never adopts
|
||||
// (only a coherent persistent one does), but a shader storage binding or
|
||||
// large-store adoption may have made the buffer resident before the map. The
|
||||
// flushed bytes then take the same landing as any other CPU write into an
|
||||
// adopted store - a persistent map already wrote them in place and only has
|
||||
// to publish the change, a non-persistent map staged them and has to land
|
||||
// them. The backend's flush op is for stores it keeps a separate copy of.
|
||||
if (m_resource.IsGpuResident()) {
|
||||
if (m_mappingAccess & BufferMappingAccessBit::Persistent) {
|
||||
NotifyContentWrite(start, length);
|
||||
} else {
|
||||
LandBytesIntoResidentStore(start, {m_stagingData.data() + m_stagingBias + offset, length});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + m_stagingBias + offset, length);
|
||||
}
|
||||
@@ -247,10 +330,51 @@ namespace MobileGL::MG_State::GLState {
|
||||
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
||||
data.size, m_size);
|
||||
|
||||
// An adopted store's Bytes() IS the memory in-flight frames are reading, and
|
||||
// GL orders a glBufferSubData after those already-submitted reads: the write
|
||||
// has to take the resident landing, never a plain host write into the mapping.
|
||||
// Shadow-backed stores need none of this: the Memcpy below touches only the
|
||||
// shadow, and the backend's SubData op does its own ordering against in-flight
|
||||
// work.
|
||||
if (m_resource.IsGpuResident()) {
|
||||
LandBytesIntoResidentStore(atOffset, data);
|
||||
return;
|
||||
}
|
||||
|
||||
Memcpy(m_resource.Bytes() + atOffset, data.data, data.size);
|
||||
NotifyContentWrite(atOffset, data.size);
|
||||
}
|
||||
|
||||
// A backend that can land the bytes on the GPU timeline takes them here, untouched
|
||||
// by the mapping - an in-place host write into coherent memory tore the frames
|
||||
// still reading the old bytes (Minecraft patches LIVE chunk sections this way).
|
||||
// The bytes are then not current in the mapping until the backend's ordered copy
|
||||
// executes, so reads reconcile through the same gate GPU-written buffers use.
|
||||
//
|
||||
// Without that op the write lands in place, after retiring the GPU writes this store
|
||||
// is known to be waiting on: a backend that defers work (DirectVulkan's frame command
|
||||
// buffer) may still be holding a recorded-but-unsubmitted dispatch that GL orders this
|
||||
// write AFTER, and writing the mapping now would land the bytes underneath that
|
||||
// dispatch - its increments then execute on top of the newer data and invert the call
|
||||
// order. That gate only knows about work that WROTE the store (MarkGpuWritten); work
|
||||
// that merely READS it - a draw sourcing an adopted vertex arena - is not tracked here,
|
||||
// so a backend without the op still owes the ordering against its own recorded reads.
|
||||
// NotifyContentWrite on a resident store only bumps the serial: the backend has no
|
||||
// separate copy to sync, so no transfer op runs.
|
||||
void BufferObject::LandBytesIntoResidentStore(SizeT offset, DataPtr bytes) {
|
||||
if (bytes.size > 0 && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||
g_bufferBackendOps->ResidentSubData(*this, offset, bytes);
|
||||
m_hasDefinedContent = true;
|
||||
++m_changeSerial;
|
||||
m_gpuWritePending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
SyncGpuWrites();
|
||||
Memcpy(m_resource.Bytes() + offset, bytes.data, bytes.size);
|
||||
NotifyContentWrite(offset, bytes.size);
|
||||
}
|
||||
|
||||
void BufferObject::FillSubData(DataPtr pattern, SizeT atOffset, SizeT size) {
|
||||
MOBILEGL_ASSERT(pattern.data != nullptr && pattern.size > 0,
|
||||
"FillSubData requires a non-empty pattern.");
|
||||
@@ -263,9 +387,29 @@ namespace MobileGL::MG_State::GLState {
|
||||
"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.
|
||||
// An adopted store takes the same landing as UploadSubData: the in-place write
|
||||
// below would tear in-flight readers of the mapping. The pattern is expanded
|
||||
// first because the landing takes the final bytes, not a repeat rule - which is
|
||||
// why only a backend that actually takes them comes through here. Without that
|
||||
// op the landing would memcpy the expansion into the mapping the loop below
|
||||
// fills in place anyway, so a whole-arena clear would allocate a whole arena
|
||||
// for nothing.
|
||||
if (m_resource.IsGpuResident() && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||
Vector<Uint8> expanded(size);
|
||||
if (pattern.size == 1) {
|
||||
Memset(expanded.data(), *static_cast<const Uint8*>(pattern.data), size);
|
||||
} else {
|
||||
for (SizeT at = 0; at < size; at += pattern.size) {
|
||||
Memcpy(expanded.data() + at, pattern.data, pattern.size);
|
||||
}
|
||||
}
|
||||
LandBytesIntoResidentStore(atOffset, {expanded.data(), size});
|
||||
return;
|
||||
}
|
||||
|
||||
// A clear is ordered after all earlier GPU writes; partial clears additionally need
|
||||
// the retained shadow bytes, and a resident store the backend cannot take the bytes
|
||||
// for is written in place, which needs the same synchronization the landing does.
|
||||
SyncGpuWrites();
|
||||
|
||||
Uint8* dst = m_resource.Bytes() + atOffset;
|
||||
@@ -299,6 +443,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
size, m_size);
|
||||
|
||||
src->SyncGpuWrites();
|
||||
// An adopted DESTINATION takes the same landing as UploadSubData: the in-place
|
||||
// write below would tear in-flight readers of the mapping, and pending recorded
|
||||
// GPU writes to it must retire before the copy lands or they would execute on
|
||||
// top of it.
|
||||
if (m_resource.IsGpuResident()) {
|
||||
LandBytesIntoResidentStore(dstOffset, {src->m_resource.Bytes() + srcOffset, size});
|
||||
return;
|
||||
}
|
||||
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
||||
NotifyContentWrite(dstOffset, size);
|
||||
}
|
||||
@@ -334,6 +486,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (m_resource.IsGpuResident()) {
|
||||
return true;
|
||||
}
|
||||
// Adoption releases the CPU shadow, and a live mapping may BE that shadow: a
|
||||
// persistent map that did not itself adopt (a FLUSH_EXPLICIT one, or a read map)
|
||||
// handed the application shadow + offset, and GL keeps that pointer valid while
|
||||
// the buffer is drawn with - which is exactly when this runs, on the storage
|
||||
// binding walk. Freeing it under the application is a use-after-free, so a mapped
|
||||
// buffer keeps the shadow model until it is unmapped; the binding that follows
|
||||
// adopts then. Same rule as TryAdoptLargeStorage.
|
||||
if (m_isMapped) {
|
||||
return false;
|
||||
}
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -352,7 +514,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
// The app is about to look at the bytes; a shader may have rewritten them since
|
||||
// the shadow was last authoritative. Also needed for a write map without an
|
||||
// invalidate bit, whose staging copy is seeded from the shadow.
|
||||
SyncGpuWrites();
|
||||
//
|
||||
// One map shape looks at nothing: a non-persistent write map that discards the
|
||||
// range it maps gets a staging copy the seeding below skips, so no reader of the
|
||||
// store exists between here and the unmap. Reconciling an ADOPTED store would
|
||||
// still cost the backend's full drain-and-wait (its queued landings are made
|
||||
// visible to the CPU by finishing the pipeline), once per map, on exactly the
|
||||
// streaming arena the adoption exists to keep cheap. The outstanding-write flag
|
||||
// stays set, so the first read that DOES look at the bytes still pays for it.
|
||||
const Bool discardsWhatItMaps =
|
||||
(access & BufferMappingAccessBit::Write) && !(access & BufferMappingAccessBit::Persistent) &&
|
||||
(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer));
|
||||
if (!(m_resource.IsGpuResident() && discardsWhatItMaps)) {
|
||||
SyncGpuWrites();
|
||||
}
|
||||
m_isMapped = true;
|
||||
m_mappingAccess = access;
|
||||
m_mappedRange = range;
|
||||
|
||||
@@ -80,6 +80,19 @@ namespace MobileGL {
|
||||
void (*Respecify)(BufferObject& bufferObject) = nullptr;
|
||||
// Contents update of [offset, offset + size) from the shadow.
|
||||
void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr;
|
||||
// Contents update of an ADOPTED (GPU-resident) store. `data` holds the app's
|
||||
// bytes, valid for the duration of the call only (a write map's staging
|
||||
// store is freed the moment the unmap that lands it returns); the frontend
|
||||
// has NOT touched the resident mapping. GL orders a
|
||||
// glBufferSubData after already-submitted GPU reads of the store, and an
|
||||
// in-place host write into the coherent mapping tears the frames still
|
||||
// reading the old bytes (Minecraft patches LIVE chunk sections this way -
|
||||
// the tear shows as one-frame wrong geometry/UVs during fast movement). The
|
||||
// backend lands the bytes on the GPU timeline instead: after in-flight
|
||||
// readers, before the next consumer. The frontend marks the buffer
|
||||
// gpu-write-pending so reads reconcile through ReadbackFromGpu. Backends
|
||||
// without this op keep the legacy ordered in-place host write.
|
||||
void (*ResidentSubData)(BufferObject& bufferObject, SizeT offset, DataPtr data) = nullptr;
|
||||
// Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the
|
||||
// app's real mapping flags so the backend can honour INVALIDATE_* /
|
||||
// UNSYNCHRONIZED semantics per call instead of merging them.
|
||||
@@ -146,9 +159,14 @@ namespace MobileGL {
|
||||
// Adopt backend host-visible coherent GPU storage as the source of truth
|
||||
// (used for GPU-written targets like transform feedback capture, so
|
||||
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
|
||||
// resident or when the backend declines.
|
||||
// resident, while the buffer is mapped (adoption releases the shadow a
|
||||
// mapping may have handed the application), or when the backend declines.
|
||||
Bool EnsureGpuResidentStorage();
|
||||
void ReleaseMemory();
|
||||
// Unmap. A write map's staged bytes land in the store on the way out, unless
|
||||
// the caller is about to replace that store (a respecification) and passes
|
||||
// false - landing them there would copy a whole mapped range into storage
|
||||
// being handed back on the next line.
|
||||
void ReleaseMemory(Bool landStagedWrites = true);
|
||||
void FlushMemoryRange(SizeT offset, SizeT length);
|
||||
|
||||
// Pushes the persistently-mapped write range to the backend; called by
|
||||
@@ -211,6 +229,10 @@ namespace MobileGL {
|
||||
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
||||
// mapping across it. See the definition for why the renewal is not optional.
|
||||
void RedefineStorage(SizeT size);
|
||||
// Backend-initiated coherent adoption for mesh-arena-sized stores; see the
|
||||
// definition for the driver behavior that makes every other write route to
|
||||
// a busy large mutable store a frame-scale stall.
|
||||
void TryAdoptLargeStorage();
|
||||
void NotifyRespecify();
|
||||
void NotifySubData(SizeT offset, SizeT size);
|
||||
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
||||
@@ -219,6 +241,12 @@ namespace MobileGL {
|
||||
// so this only bumps the change serial; otherwise it dispatches a backend
|
||||
// SubData transfer to sync the backend's separate GPU copy.
|
||||
void NotifyContentWrite(SizeT offset, SizeT size);
|
||||
// The one route CPU-sourced bytes take into an ADOPTED (GPU-resident) store:
|
||||
// glBufferSubData, a buffer clear, a buffer copy, and the landing of a
|
||||
// non-persistent write map at unmap / explicit flush all go through it, so
|
||||
// the routes cannot drift apart again. Carries no mapping asserts on
|
||||
// purpose - the unmap landing runs while the buffer is still mapped.
|
||||
void LandBytesIntoResidentStore(SizeT offset, DataPtr bytes);
|
||||
|
||||
static Uint64 AllocateLifetimeId();
|
||||
|
||||
|
||||
@@ -19,7 +19,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
BufferTarget::DrawIndirect, BufferTarget::Parameter, BufferTarget::ShaderStorage);
|
||||
constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback,
|
||||
BufferTarget::AtomicCounter, BufferTarget::ShaderStorage);
|
||||
constexpr SizeT BufferBindingPointCount = 36;
|
||||
// How many indexed binding points each of BufferBindPointTargets gets. 84 is the GL 4.5 core
|
||||
// minimum for GL_MAX_UNIFORM_BUFFER_BINDINGS (table 23.64) and this array is the capacity
|
||||
// that limit is clamped against - at 36 the clamp in GL_Getter was degenerate (lo == hi) and
|
||||
// no application could ever be told about, or bind to, a binding point past the 36th. The
|
||||
// other three targets advertise their own, smaller ceilings out of
|
||||
// GetIndexedBufferQueryPointCount, so widening this does not widen what they promise; it only
|
||||
// costs the unused tail of three arrays.
|
||||
constexpr SizeT BufferBindingPointCount = 84;
|
||||
|
||||
class BufferState {
|
||||
public:
|
||||
|
||||
@@ -267,6 +267,13 @@ namespace MobileGL::MG_State {
|
||||
return m_textureState.CreateTextureObject(index, target);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
|
||||
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
|
||||
Uint numLevels, Uint minLayer, Uint numLayers) {
|
||||
return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
|
||||
numLayers);
|
||||
}
|
||||
|
||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
|
||||
// that is currently bound acts as if FramebufferTexture* had been called with texture
|
||||
@@ -643,6 +650,12 @@ namespace MobileGL::MG_State {
|
||||
// a graphics program carrying a compute module, which Adreno 830 does not reject
|
||||
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
|
||||
Bool anyStage = false;
|
||||
// Which stages the composite ACTUALLY got a shader for. Not the same question as
|
||||
// "which stages have a stage program bound": one program bound with
|
||||
// GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
|
||||
// stages it was linked with. The transform-feedback capture stage is chosen off this,
|
||||
// because it has to be the stage that will exist in the composite's own link.
|
||||
Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
@@ -658,9 +671,64 @@ namespace MobileGL::MG_State {
|
||||
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
|
||||
composite->AttachShaderWithPinnedLinkInput(ref);
|
||||
anyStage = true;
|
||||
compositeHasStage[stage] = true;
|
||||
}
|
||||
}
|
||||
if (!anyStage) return nullProgram;
|
||||
// Transform feedback captures the output of the LAST vertex-processing stage
|
||||
// (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
|
||||
// only the stage program carrying that stage can have been given. The composite is
|
||||
// assembled out of the stage programs' shaders and inherits none of their
|
||||
// GL-thread-owned state, so without this it links with an empty capture list and
|
||||
// glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
|
||||
// no transform feedback varyings") even though glValidateProgramPipeline had passed.
|
||||
//
|
||||
// TWO RULES, both easy to get subtly wrong and both load-bearing:
|
||||
//
|
||||
// (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
|
||||
// take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
|
||||
// deliberately bumps no version - so a request written after the stage program's
|
||||
// last link is invisible to the composite cache's signature yet would be picked up
|
||||
// by the next rebuild, making the capture list depend on whether some unrelated
|
||||
// event happened to invalidate the cache. Worse, a name that is not an output of
|
||||
// the capture stage fails the composite's OWN link, and a failed composite makes
|
||||
// every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
|
||||
// snapshot removes the whole class: linked state only moves at a link, and a link
|
||||
// is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
|
||||
// the existing cache key is sufficient by construction.
|
||||
// GetTransformFeedbackInterfaceNames() is the right accessor rather than the
|
||||
// resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
|
||||
// (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
|
||||
//
|
||||
// (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
|
||||
// the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
|
||||
// getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
|
||||
// WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
|
||||
// Skipping a geometry stage that has no capture list and installing the vertex
|
||||
// stage's instead made them disagree, and the composite then resolved a vertex
|
||||
// program's names against the geometry intermediate - capturing where GL says it
|
||||
// must not, or failing the link and killing every draw. A capture stage with an
|
||||
// empty list is not a reason to look further down: it is the answer, and
|
||||
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
|
||||
//
|
||||
// The order is the pipeline read backwards and includes the tessellation CONTROL
|
||||
// stage, which is a vertex-processing stage too (GL 4.6 core 11): it can only be
|
||||
// the last one in a pipeline that has a TCS but no evaluation or geometry stage,
|
||||
// which is why it sits after TessEval. Same four stages, same order, as
|
||||
// ProgramLinkTask::ResolveTransformFeedbackVaryings - see rule (2).
|
||||
for (const ShaderStage captureStage:
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
|
||||
ShaderStage::Vertex}) {
|
||||
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
|
||||
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
|
||||
if (!captureProgram) continue;
|
||||
const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
|
||||
if (!linkedNames.empty()) {
|
||||
composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
|
||||
captureProgram->GetTransformFeedbackBufferMode());
|
||||
}
|
||||
break;
|
||||
}
|
||||
// A pipeline with no fragment stage still rasterises, so the default fragment
|
||||
// shader is wanted here even though the separable stage programs never get one.
|
||||
composite->Link(true);
|
||||
@@ -805,6 +873,22 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetPatchVertices(vertices);
|
||||
}
|
||||
|
||||
void GLContext::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
|
||||
m_renderState.SetPatchDefaultOuterLevel(levels);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetPatchDefaultOuterLevel() const {
|
||||
return m_renderState.GetPatchDefaultOuterLevel();
|
||||
}
|
||||
|
||||
void GLContext::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
|
||||
m_renderState.SetPatchDefaultInnerLevel(levels);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetPatchDefaultInnerLevel() const {
|
||||
return m_renderState.GetPatchDefaultInnerLevel();
|
||||
}
|
||||
|
||||
Uint GLContext::GetPatchVertices() const {
|
||||
return m_renderState.GetPatchVertices();
|
||||
}
|
||||
@@ -825,6 +909,26 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetPolygonOffsetUnits();
|
||||
}
|
||||
|
||||
void GLContext::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
|
||||
m_renderState.SetPolygonOffsetClamped(factor, units, clamp);
|
||||
}
|
||||
|
||||
Float GLContext::GetPolygonOffsetClamp() const {
|
||||
return m_renderState.GetPolygonOffsetClamp();
|
||||
}
|
||||
|
||||
void GLContext::SetClipControl(GLenum origin, GLenum depth) {
|
||||
m_renderState.SetClipControl(origin, depth);
|
||||
}
|
||||
|
||||
GLenum GLContext::GetClipOrigin() const {
|
||||
return m_renderState.GetClipOrigin();
|
||||
}
|
||||
|
||||
GLenum GLContext::GetClipDepthMode() const {
|
||||
return m_renderState.GetClipDepthMode();
|
||||
}
|
||||
|
||||
void GLContext::SetCapability(CapabilityInput cap, Bool enabled) {
|
||||
m_renderState.SetCapability(cap, enabled);
|
||||
}
|
||||
@@ -1002,6 +1106,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetSampleMaskValue();
|
||||
}
|
||||
|
||||
void GLContext::SetMinSampleShadingValue(Float value) {
|
||||
m_renderState.SetMinSampleShadingValue(value);
|
||||
}
|
||||
|
||||
Float GLContext::GetMinSampleShadingValue() const {
|
||||
return m_renderState.GetMinSampleShadingValue();
|
||||
}
|
||||
|
||||
void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) {
|
||||
m_renderState.SetPixelStoreParam(param, value);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,11 @@ namespace MobileGL {
|
||||
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
// See TextureState::CreateTextureViewObject (glTextureView, GL 4.6 core 8.18).
|
||||
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
|
||||
const SharedPtr<ITextureObject>& storageOwner,
|
||||
Uint minLevel, Uint numLevels, Uint minLayer,
|
||||
Uint numLayers);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
@@ -208,9 +213,18 @@ namespace MobileGL {
|
||||
Float GetPointSize() const;
|
||||
void SetPatchVertices(Uint vertices);
|
||||
Uint GetPatchVertices() const;
|
||||
void SetPatchDefaultOuterLevel(const FloatVec4& levels);
|
||||
const FloatVec4& GetPatchDefaultOuterLevel() const;
|
||||
void SetPatchDefaultInnerLevel(const FloatVec2& levels);
|
||||
const FloatVec2& GetPatchDefaultInnerLevel() const;
|
||||
void SetPolygonOffset(Float factor, Float units);
|
||||
void SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
|
||||
Float GetPolygonOffsetFactor() const;
|
||||
Float GetPolygonOffsetUnits() const;
|
||||
Float GetPolygonOffsetClamp() const;
|
||||
void SetClipControl(GLenum origin, GLenum depth);
|
||||
GLenum GetClipOrigin() const;
|
||||
GLenum GetClipDepthMode() const;
|
||||
void SetHint(GLenum target, GLenum mode);
|
||||
GLenum GetHint(GLenum target) const;
|
||||
void SetPointFadeThresholdSize(Float size);
|
||||
@@ -271,6 +285,8 @@ namespace MobileGL {
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
void SetSampleMaskValue(Uint32 mask);
|
||||
Uint32 GetSampleMaskValue() const;
|
||||
void SetMinSampleShadingValue(Float value);
|
||||
Float GetMinSampleShadingValue() const;
|
||||
void SetPixelStoreParam(PixelStoreParam param, Int value);
|
||||
Int GetPixelStoreParam(PixelStoreParam param) const;
|
||||
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -29,13 +30,11 @@ namespace {
|
||||
// capacity, which is also the width of the Uint32 masks backends build from it.
|
||||
static MobileGL::Int GetReflectionVertexAttribLimit(
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
constexpr MobileGL::Int capacity =
|
||||
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!env.HasBackend()) return capacity;
|
||||
|
||||
const MobileGL::Int backendLimit = env.params.MaxVertexAttribs;
|
||||
if (backendLimit <= 0) return capacity;
|
||||
return std::min(backendLimit, capacity);
|
||||
// One shared definition with glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) and with
|
||||
// BuildTBuiltInResource's gl_MaxVertexAttribs - the three used to carry three copies of
|
||||
// this formula and glslang's copy was a hardcoded 64.
|
||||
return MobileGL::MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(env.HasBackend(),
|
||||
env.params.MaxVertexAttribs);
|
||||
}
|
||||
|
||||
// Everything the post-link query surface ever asks a glslang::TType, flattened into a
|
||||
@@ -518,7 +517,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void ProgramLinkTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
||||
void ProgramLinkTask::DeferLog(String line, const Int level) {
|
||||
diagnostics.logLines.push_back({level, Move(line)});
|
||||
}
|
||||
|
||||
void ProgramLinkTask::SubmitAfter(const Vector<SharedPtr<ShaderCompileTask>>& deps) {
|
||||
// +1 for the guard this function releases itself. Without it, a dependency that
|
||||
@@ -622,12 +623,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
// mapper's collect callback is the last point at which a resource's qualifier still
|
||||
// says what the SHADER declared rather than what glslang assigned, so both captures
|
||||
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
|
||||
// The binding-range rule (GLSL 4.30 4.4.5): its ceilings in, and the first violation the
|
||||
// resolver finds out. Enforced at the link because mapIO's collect callback is the last
|
||||
// point at which a resource's qualifier still says what the SHADER declared - see
|
||||
// TMglGlslIoResolver::CheckDeclaredBindingRange.
|
||||
String resourceBindingViolation;
|
||||
ProgramAttrib attrib{.shaders = Move(shaders),
|
||||
.explicitVertexInLocations = in.explicitAttribLocations,
|
||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
|
||||
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding,
|
||||
.resourceBindingLimits = in.env ? ResolveResourceBindingLimits(*in.env)
|
||||
: MG_Util::ShaderTranspiler::ResourceBindingLimits{},
|
||||
.resourceBindingViolation = &resourceBindingViolation};
|
||||
|
||||
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
||||
auto result = ShaderCompiler::LinkProgram(attrib);
|
||||
@@ -671,9 +681,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
|
||||
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
|
||||
// The geometry stage's link properties. GL_GEOMETRY_INPUT_TYPE is load-bearing beyond the
|
||||
// query surface - a draw's primitive type has to be compatible with it (GL 4.6 core
|
||||
// 11.3.1) - so this block runs for every link, not only a capturing one. The other three
|
||||
// are pure glGetProgramiv answers that previously had no source at all.
|
||||
artifacts.gsInputPrimitive = GL_NONE;
|
||||
artifacts.gsOutputPrimitive = GL_NONE;
|
||||
artifacts.gsMaxVertices = 0;
|
||||
artifacts.gsInvocations = 0;
|
||||
if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) {
|
||||
switch (gs->getInputPrimitive()) {
|
||||
case glslang::ElgPoints: artifacts.gsInputPrimitive = GL_POINTS; break;
|
||||
@@ -683,6 +698,77 @@ namespace MobileGL::MG_State::GLState {
|
||||
case glslang::ElgTrianglesAdjacency: artifacts.gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break;
|
||||
default: break;
|
||||
}
|
||||
switch (gs->getOutputPrimitive()) {
|
||||
case glslang::ElgPoints: artifacts.gsOutputPrimitive = GL_POINTS; break;
|
||||
case glslang::ElgLineStrip: artifacts.gsOutputPrimitive = GL_LINE_STRIP; break;
|
||||
case glslang::ElgTriangleStrip: artifacts.gsOutputPrimitive = GL_TRIANGLE_STRIP; break;
|
||||
default: break;
|
||||
}
|
||||
// glslang leaves both at TQualifier::layoutNotSet (-1) when the shader declared no
|
||||
// such layout, and `invocations` defaults to one per GLSL 4.60 4.4.2.2 - so clamp
|
||||
// rather than forward, or GL_GEOMETRY_SHADER_INVOCATIONS reports the sentinel.
|
||||
artifacts.gsMaxVertices = std::max(gs->getVertices(), 0);
|
||||
artifacts.gsInvocations = std::max(gs->getInvocations(), 1);
|
||||
}
|
||||
|
||||
// The tessellation evaluation stage's link properties, GL 4.6 core table 23.35: the
|
||||
// primitive generator's mode, spacing, winding and point mode. (The control stage's
|
||||
// output patch size is captured below, together with the limit check that goes with it.)
|
||||
artifacts.tessGenMode = GL_NONE;
|
||||
artifacts.tessGenSpacing = GL_NONE;
|
||||
artifacts.tessGenVertexOrder = GL_NONE;
|
||||
artifacts.tessGenPointMode = false;
|
||||
if (const glslang::TIntermediate* tes = artifacts.program->getIntermediate(EShLangTessEvaluation)) {
|
||||
switch (tes->getInputPrimitive()) {
|
||||
case glslang::ElgTriangles: artifacts.tessGenMode = GL_TRIANGLES; break;
|
||||
case glslang::ElgQuads: artifacts.tessGenMode = GL_QUADS; break;
|
||||
case glslang::ElgIsolines: artifacts.tessGenMode = GL_ISOLINES; break;
|
||||
default: break;
|
||||
}
|
||||
// GLSL 4.60 4.4.2.3: equal_spacing and ccw are the defaults, which is what an unset
|
||||
// qualifier means here.
|
||||
switch (tes->getVertexSpacing()) {
|
||||
case glslang::EvsFractionalEven: artifacts.tessGenSpacing = GL_FRACTIONAL_EVEN; break;
|
||||
case glslang::EvsFractionalOdd: artifacts.tessGenSpacing = GL_FRACTIONAL_ODD; break;
|
||||
default: artifacts.tessGenSpacing = GL_EQUAL; break;
|
||||
}
|
||||
switch (tes->getVertexOrder()) {
|
||||
case glslang::EvoCw: artifacts.tessGenVertexOrder = GL_CW; break;
|
||||
default: artifacts.tessGenVertexOrder = GL_CCW; break;
|
||||
}
|
||||
artifacts.tessGenPointMode = tes->getPointMode();
|
||||
}
|
||||
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES, i.e. the `layout(vertices = N) out` the control stage
|
||||
// declared, and the limit that goes with it.
|
||||
//
|
||||
// GL 4.6 core 11.2.1.1: the LINK fails when N is greater than MAX_PATCH_VERTICES. Nothing
|
||||
// enforced it - glslang's layout handling only rejects N <= 0 (ParseHelper.cpp "must be
|
||||
// greater than 0") and carries maxPatchVertices in TBuiltInResource purely so
|
||||
// gl_MaxPatchVertices can expand from it, exactly the gap ValidateImageUniformLimits
|
||||
// documents for image uniforms. Checked at LINK rather than at compile on purpose: the CTS
|
||||
// requires the offending shader to COMPILE ("Compilation passed as allowed") and only the
|
||||
// link to fail, and turning it into a parse error would newly break an application that
|
||||
// compiles such a shader and never links it.
|
||||
//
|
||||
// The limit is the one glGetIntegerv answers (GL_Getter.cpp reads the same
|
||||
// DynamicBackendParameters field), so the advertised number and the enforced number cannot
|
||||
// drift apart.
|
||||
artifacts.tcsOutputVertices = 0;
|
||||
if (const glslang::TIntermediate* tcs = artifacts.program->getIntermediate(EShLangTessControl)) {
|
||||
artifacts.tcsOutputVertices = static_cast<Int>(tcs->getVertices());
|
||||
if (artifacts.tcsOutputVertices > env.params.MaxPatchVertices) {
|
||||
artifacts.linkStatus = false;
|
||||
// Same invariant as the compute local-size gate above: a rejected link leaves no
|
||||
// TProgram behind for a query surface to find.
|
||||
artifacts.program.reset();
|
||||
artifacts.infoLog = std::format(
|
||||
"Tessellation control shader declares an output patch of {} vertices, more than the {} "
|
||||
"GL_MAX_PATCH_VERTICES allows.",
|
||||
artifacts.tcsOutputVertices, env.params.MaxPatchVertices);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
|
||||
@@ -771,6 +857,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
|
||||
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
|
||||
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
|
||||
// The capture set is NOT part of that slice (see the handoff's own comment), and the
|
||||
// point-size demotion needs exactly one bit out of it: whether anything asked to
|
||||
// capture gl_PointSize. Derived here, where ResolveTransformFeedbackVaryings has
|
||||
// just filled artifacts.xfbVaryings and before the join moves them away, because a
|
||||
// capture stage that only READS the built-in still has to declare the carrier the
|
||||
// capture binds to - and phase B has no other way to learn that.
|
||||
for (const ProgramObject::XfbVarying& varying : artifacts.xfbVaryings) {
|
||||
if (varying.name == "gl_PointSize") {
|
||||
spirvHandoff.captureRequestsPointSize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
|
||||
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
|
||||
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
|
||||
@@ -812,6 +910,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
|
||||
// disagree.
|
||||
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
|
||||
// The second and third capability bits, under exactly the same rule: each arms a
|
||||
// phase-B rewrite of the cached modules (the point-size demotion), read from the
|
||||
// same env snapshot that phase B will consult, so key and bytes cannot disagree.
|
||||
keyInputs.demoteTessellationPointSize = env.DemotesTessellationPointSize();
|
||||
keyInputs.demoteGeometryPointSize = env.DemotesGeometryPointSize();
|
||||
keyInputs.stages.reserve(in.shaders.size());
|
||||
for (const LinkShaderInput& shader : in.shaders) {
|
||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||
@@ -1103,6 +1206,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
// The gl_NumSamples stand-in InjectNumSamplesBuiltinShim declared. It is a driver
|
||||
// uniform, not the application's: gl_NumSamples is a BUILT-IN, so a conformant
|
||||
// implementation reports nothing for it in GL_ACTIVE_UNIFORMS, glGetActiveUniform or
|
||||
// glGetUniformLocation, and nothing may write it through glUniform* either. Filtering
|
||||
// it here does both, and costs it no storage: BuildGlobalUboRouting takes its offset
|
||||
// from the SPIR-V metadata by name, not from the GL location space.
|
||||
if (isGlobalUboMember(uniform) &&
|
||||
uniform.name == MG_Util::ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) {
|
||||
artifacts.usesReservedNumSamples = true;
|
||||
MGLOG_D("ProgramObject %u: Reflection - reserved gl_NumSamples stand-in '%s' hidden from the GL "
|
||||
"uniform surface",
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
if (isBufferVariable(uniform)) {
|
||||
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
|
||||
"surface",
|
||||
@@ -1573,9 +1690,46 @@ namespace MobileGL::MG_State::GLState {
|
||||
// (DirectGLES.cpp / UniformManager.cpp), all 14 elements also read the same
|
||||
// buffer. This is the rule the storage-block path in ProgramInterface.cpp
|
||||
// already applies, and whose comment there claims uniform blocks follow.
|
||||
const Int declaredBinding = ubo.getBinding();
|
||||
//
|
||||
// "Declared" cannot be read back off the reflection, though. MobileGL asks glslang
|
||||
// to auto-map bindings, so mapIO writes an invented one into every block's
|
||||
// qualifier before reflection ever runs and ubo.getBinding() is never negative;
|
||||
// worse, glslang packs uniform blocks into the SAME slot space as samplers and
|
||||
// images (setEnvClient(EShClientVulkan) leaves spvVersion.openGl at 0, so
|
||||
// TDefaultGlslIoResolver::resolveBinding keys every resource kind on set 0), so a
|
||||
// block declared after an unbound image gets 1. GL 4.6 core 7.6.2 says an
|
||||
// unqualified block reports ZERO. The set below is the shader's own answer,
|
||||
// captured during mapIO while the qualifier still meant it - the same mechanism
|
||||
// SeedDefaultStorageBlockBindings uses for storage blocks, and the aliasing at 0
|
||||
// that results is GL's, not a bug: unqualified blocks collide there until the
|
||||
// application rebinds them.
|
||||
//
|
||||
// Only this GL-visible binding POINT changes. The backends' descriptor lookups run
|
||||
// off glslang's assignment through uniformBlockIndexByBinding, which is untouched.
|
||||
const String blockTypeName = StripArrayElementSuffix(ubo.name);
|
||||
const Int declaredBinding =
|
||||
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
|
||||
artifacts.uniformBlockBinding[i] =
|
||||
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
|
||||
// The second way a binding reaches the state layer's indexed-binding array, and the
|
||||
// one glUniformBlockBinding's new bound cannot see. glslang does not range-check a
|
||||
// uniform block's layout(binding = N) against anything - TBuiltInResource has no
|
||||
// maxUniformBufferBindings field at all, and ParseHelper bounds only samplers and
|
||||
// atomic counters - so `layout(binding = 5000) uniform Blk {...}` compiled and linked
|
||||
// clean and then had both backends subscript the array at 5000 on the first draw.
|
||||
// Stated against the same ceiling glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS)
|
||||
// advertises; an instance array whose LAST element passes it is a link error even
|
||||
// though its base fits, same rule as the explicit-location check above.
|
||||
if (artifacts.uniformBlockBinding[i] >=
|
||||
static_cast<Int>(MG_State::GLState::BufferBindingPointCount)) {
|
||||
artifacts.infoLog =
|
||||
std::format("Uniform block '{}' declares binding {}, which is not less than "
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).",
|
||||
ubo.name, artifacts.uniformBlockBinding[i],
|
||||
static_cast<Int>(MG_State::GLState::BufferBindingPointCount));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
||||
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
||||
}
|
||||
@@ -1721,7 +1875,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// them to the draw-buffer range fails the link of every such program.
|
||||
if (artifacts.program->getIntermediate(EShLangFragment) == nullptr) return true;
|
||||
|
||||
UnorderedMap<Int, String> colorNumberOwners;
|
||||
// Keyed on (colour number, COLOUR INDEX), not on the colour number alone. Two fragment
|
||||
// outputs may share a location as long as their index differs - that pair IS dual-source
|
||||
// blending (GL 4.6 core 11.1.3 / ARB_blend_func_extended, core since 3.3), spelled either
|
||||
// `layout(location = 0, index = 0)` + `layout(location = 0, index = 1)` in the shader or
|
||||
// through two glBindFragDataLocationIndexed calls. Aliasing on the number alone made every
|
||||
// such program fail to link with "alias color number 0", which is the whole feature.
|
||||
UnorderedMap<Int64, String> colorSlotOwners;
|
||||
const Int outputCount = artifacts.program->getNumPipeOutputs();
|
||||
for (Int index = 0; index < outputCount; ++index) {
|
||||
const auto& output = artifacts.program->getPipeOutput(index);
|
||||
@@ -1734,6 +1894,46 @@ namespace MobileGL::MG_State::GLState {
|
||||
const Int location = explicitLocation != in.explicitFragDataLocation.end()
|
||||
? static_cast<Int>(explicitLocation->second)
|
||||
: static_cast<Int>(output.layoutLocation());
|
||||
// The colour INDEX, under the one precedence rule the whole codebase uses: a NON-ZERO
|
||||
// glBindFragDataLocationIndexed index wins, and a zero (or absent) one falls back to
|
||||
// the shader's own layout(index = N).
|
||||
//
|
||||
// Zero has to mean "no override" rather than "index 0", because glBindFragDataLocation
|
||||
// IS glBindFragDataLocationIndexed with index 0 (GL_Program.cpp) and writes a real 0
|
||||
// into this map. Reading that 0 as an override made a blanket
|
||||
// `glBindFragDataLocation(prog, 0, "b")` over a shader that declares
|
||||
// `layout(location = 0, index = 1) out vec4 b;` collapse b onto slot (0,0) next to the
|
||||
// index-0 output and fail the link as an alias - while the IO resolver had left b's
|
||||
// qualifier at 1, the SPIR-V still carried Index 1, and glGetProgramResourceLocationIndex
|
||||
// still answered 1. Validation was rejecting a program the backend had already emitted
|
||||
// correctly, which is the one case where this branch can change the answer at all: this
|
||||
// runs AFTER ShaderCompiler::LinkProgram/mapIO, so for every other shape the qualifier
|
||||
// already carries the resolver's verdict.
|
||||
//
|
||||
// The two other consumers spell the same rule: TMglGlslIoResolver only writes the API
|
||||
// index into the qualifier when it is non-zero, and ProgramInterface falls back to
|
||||
// type.layoutIndex when GetFragmentDataIndex answers 0. All three now agree.
|
||||
//
|
||||
// Against the spec (GL 4.6 core 15.2.3): where a fragment output's index is given by a
|
||||
// shader layout qualifier, that value is used and anything bound through
|
||||
// BindFragDataLocation(Indexed) is IGNORED - the same precedence layout(location) has
|
||||
// over glBindAttribLocation. That is stricter than "non-zero API wins", and the two
|
||||
// differ in exactly one shape: an explicit `index = 0` in the shader against an API
|
||||
// index of 1, where the spec keeps 0 and this codebase takes 1. That divergence lives
|
||||
// in the resolver (it decides what is emitted); it is pre-existing, out of scope here,
|
||||
// and deliberately not re-litigated in a third place - matching the resolver is what
|
||||
// keeps validation checking what was actually built.
|
||||
Int colorIndex = 0;
|
||||
if (const auto explicitIndex = in.explicitFragDataIndex.find(outputName);
|
||||
explicitIndex != in.explicitFragDataIndex.end()) {
|
||||
colorIndex = static_cast<Int>(explicitIndex->second);
|
||||
}
|
||||
if (colorIndex == 0) {
|
||||
if (const glslang::TType* outputType = output.getType();
|
||||
outputType != nullptr && outputType->getQualifier().hasIndex()) {
|
||||
colorIndex = static_cast<Int>(outputType->getQualifier().layoutIndex);
|
||||
}
|
||||
}
|
||||
const Int span = std::max<Int>(output.size, 1);
|
||||
|
||||
if (location < 0 || location + span > in.maxFragmentOutputColorNumber) {
|
||||
@@ -1746,10 +1946,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
for (Int colorNumber = location; colorNumber < location + span; ++colorNumber) {
|
||||
auto [owner, inserted] = colorNumberOwners.emplace(colorNumber, outputName);
|
||||
const Int64 slot = (static_cast<Int64>(colorIndex) << 32) |
|
||||
static_cast<Int64>(static_cast<Uint32>(colorNumber));
|
||||
auto [owner, inserted] = colorSlotOwners.emplace(slot, outputName);
|
||||
if (!inserted) {
|
||||
artifacts.infoLog = std::format("Fragment outputs '{}' and '{}' alias color number {}.",
|
||||
owner->second, outputName, colorNumber);
|
||||
artifacts.infoLog =
|
||||
colorIndex == 0
|
||||
? std::format("Fragment outputs '{}' and '{}' alias color number {}.", owner->second,
|
||||
outputName, colorNumber)
|
||||
: std::format("Fragment outputs '{}' and '{}' alias color number {} at index {}.",
|
||||
owner->second, outputName, colorNumber, colorIndex);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
@@ -1775,10 +1981,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capture happens at the last vertex-processing stage (geometry, then
|
||||
// tessellation evaluation, then vertex).
|
||||
// Capture happens at the last vertex-processing stage (geometry, then tessellation
|
||||
// evaluation, then tessellation CONTROL, then vertex). All four are vertex-processing
|
||||
// stages in GL 4.6 core 11 - the control shader included - and in a separable program
|
||||
// whose only stage is a TCS it is the last one that exists, so it is the capture stage
|
||||
// and such a program MUST link (GL 4.6 core 7.3/11.1.2.1; the conformance suite spells
|
||||
// the API split out at esextcTessellationShaderXFB.cpp:390-416, where a non-ES context
|
||||
// takes should_succeed=true). TessControl sits AFTER TessEvaluation so a complete
|
||||
// pipeline still captures at the evaluation stage and only a TCS-only program falls
|
||||
// through to it. If MobileGL ever serves an ES context this arm has to be gated on the
|
||||
// advertised API: ES requires the very same link to FAIL.
|
||||
const glslang::TIntermediate* captureIntermediate = nullptr;
|
||||
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
|
||||
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangTessControl, EShLangVertex}) {
|
||||
captureIntermediate = artifacts.program->getIntermediate(stage);
|
||||
if (captureIntermediate != nullptr) {
|
||||
break;
|
||||
|
||||
@@ -117,6 +117,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
// for phase B after the join has moved `artifacts` away.
|
||||
ProgramObject::LinkArtifacts reflection;
|
||||
|
||||
// Whether the RESOLVED transform-feedback capture set names gl_PointSize - the
|
||||
// one fact about `artifacts.xfbVaryings` phase B needs, carried as a derived
|
||||
// bool rather than by widening the slice above, which is deliberately the five
|
||||
// (now eight) fields BuildGlobalUboRouting consumes and nothing else.
|
||||
//
|
||||
// It has to be here and cannot be re-derived: the point-size demotion forces the
|
||||
// capture-capable stage to declare its carrier even when that stage never WRITES
|
||||
// the built-in (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram's
|
||||
// `captureRequestsPointSize`), and by phase B the only record of the request is
|
||||
// this bit. No new L1 key material: the key already covers
|
||||
// `requestedXfbVaryings`, of which this is a function.
|
||||
Bool captureRequestsPointSize = false;
|
||||
|
||||
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||
// A, and not by phase B - two reasons, both structural:
|
||||
@@ -201,8 +214,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
||||
// join, on the GL thread, where a serial implementation would have printed it.
|
||||
// Logging straight from a worker interleaves mid-line with the GL thread's output and
|
||||
// lands out of order relative to the glLinkProgram that caused it.
|
||||
void DeferLog(String line);
|
||||
// lands out of order relative to the glLinkProgram that caused it. `level` is the
|
||||
// severity the replay uses; DEBUG (the default) is compiled out of every shipped
|
||||
// build, so a line that has to survive one names its own.
|
||||
void DeferLog(String line, Int level = MOBILEGL_LOG_LEVEL_DEBUG);
|
||||
|
||||
// Counts down to zero exactly once. Starts at deps + 1: the extra guard is released
|
||||
// by SubmitAfter itself, so a dependency that settles while the edges are still being
|
||||
|
||||
@@ -369,6 +369,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// link, so a stale set would otherwise default a block the new sources do declare a
|
||||
// binding for.
|
||||
artifacts.storageBlocksWithoutBinding.clear();
|
||||
artifacts.uniformBlocksWithoutBinding.clear();
|
||||
artifacts.attribs.clear();
|
||||
artifacts.attribTypes.clear();
|
||||
artifacts.activeUniformCount = 0;
|
||||
@@ -491,6 +492,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// time, for anything cached during the pending window itself.)
|
||||
++m_backendStateVersion;
|
||||
BumpLinkObservableVersions();
|
||||
// The separable flag takes effect HERE, at the link, and nowhere else (GL 4.6 core 7.3).
|
||||
// Latched before the early-outs below so a link that fails still counts as a link -
|
||||
// what must not update it is a link that never happened at all.
|
||||
m_linkedSeparable = m_separable;
|
||||
// A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this
|
||||
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
||||
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
||||
@@ -535,6 +540,34 @@ namespace MobileGL::MG_State::GLState {
|
||||
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
|
||||
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
|
||||
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
|
||||
// ARB_gl_spirv: a program built from SPIR-V declares its transform feedback through
|
||||
// XfbBuffer/XfbStride/Offset DECORATIONS, and glTransformFeedbackVaryings has no effect on
|
||||
// it at all. glSpecializeShader translated those decorations into the equivalent name
|
||||
// request (ShaderCompiler::SpecializeAndDecompileSpirvModule), and this is where it enters
|
||||
// the link - so everything downstream, the frontend packer and both backends, sees one
|
||||
// declaration form instead of two.
|
||||
//
|
||||
// The capture stage is the LAST vertex-processing stage the program has, which is the same
|
||||
// rule ProgramLinkTask::ResolveTransformFeedbackVaryings resolves the names against. The
|
||||
// application's own request wins if it made one: that can only happen on a mixed program,
|
||||
// which is not a shape ARB_gl_spirv defines, and honouring what the application explicitly
|
||||
// asked for is the safer of the two readings.
|
||||
if (task->in.requestedXfbVaryings.empty()) {
|
||||
for (const ShaderStage captureStage:
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
|
||||
ShaderStage::Vertex}) {
|
||||
Bool stagePresent = false;
|
||||
for (const auto& shader : m_shaders) {
|
||||
if (!shader || shader->GetShaderStage() != captureStage) continue;
|
||||
stagePresent = true;
|
||||
if (shader->GetSpirvXfbVaryings().empty()) continue;
|
||||
task->in.requestedXfbVaryings = shader->GetSpirvXfbVaryings();
|
||||
task->in.requestedXfbBufferMode = shader->GetSpirvXfbBufferMode();
|
||||
break;
|
||||
}
|
||||
if (stagePresent) break;
|
||||
}
|
||||
}
|
||||
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
|
||||
|
||||
Vector<SharedPtr<ShaderCompileTask>> deps;
|
||||
|
||||
@@ -603,6 +603,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// other question about the global UBO's layout - and it is one: it decides how wide a
|
||||
// `double` uniform's slot is.
|
||||
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
|
||||
// Whether gl_PointSize was demoted out of this program's tessellation/geometry
|
||||
// modules into the ordinary carrier varying. Joins phase B: it is a fact about the
|
||||
// generated modules, and its readers (the backends' capture-name respelling) already
|
||||
// hold the phase-B join.
|
||||
Bool PointSizeDemoted() const { return Spirv().pointSizeDemoted; }
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
|
||||
UsesNativeFloat64());
|
||||
@@ -787,6 +792,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
void MarkUBOContentDirty() const {
|
||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||
}
|
||||
|
||||
// ---- the reserved gl_NumSamples stand-in (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) ----
|
||||
//
|
||||
// PHASE A: answerable without joining the SPIR-V job, which is what lets the draw path ask
|
||||
// every program this question and pay nothing for the overwhelming majority that say no.
|
||||
Bool UsesReservedNumSamples() const { return Artifacts().usesReservedNumSamples; }
|
||||
|
||||
// Publishes `samples` into the global-UBO shadow. Returns false when there is nowhere to
|
||||
// put it - no shim in this program, no SPIR-V (a cancelled phase B), or the optimizer
|
||||
// dropped the member because nothing read it after all - all of which are ordinary states,
|
||||
// not errors. A value-identical write is dropped without bumping the content version, so a
|
||||
// steady stream of draws into one framebuffer does not force a re-upload per draw.
|
||||
Bool WriteReservedNumSamples(Int samples) {
|
||||
if (!UsesReservedNumSamples()) return false;
|
||||
SpirvArtifacts& spirv = Spirv();
|
||||
const Uint offset = spirv.reservedNumSamplesOffset;
|
||||
if (offset == kInvalidUniformOffset) return false;
|
||||
if (static_cast<SizeT>(offset) + sizeof(Int) > spirv.globalUboScratch.size()) return false;
|
||||
|
||||
Uint8* const slot = spirv.globalUboScratch.data() + offset;
|
||||
Int current = 0;
|
||||
Memcpy(¤t, slot, sizeof(Int));
|
||||
if (current == samples) return true;
|
||||
Memcpy(slot, &samples, sizeof(Int));
|
||||
MarkUBOContentDirty();
|
||||
return true;
|
||||
}
|
||||
// ---- glUniform* inside the phase-A -> phase-B window ----
|
||||
//
|
||||
// True while the program is fully linked and fully queryable but its uniform shadow's
|
||||
@@ -888,6 +920,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// subset of the stages of a program pipeline. Only takes effect on the next link,
|
||||
// which is why it is plain state here rather than something Link() consults.
|
||||
Bool GetSeparable() const { return m_separable; }
|
||||
// What GL_PROGRAM_SEPARABLE actually reports, and what glUseProgramStages actually
|
||||
// requires: the value the flag held at the program's LAST LINK, not the live flag.
|
||||
// GL 4.6 core 7.3 - "the flag takes effect the next time the program is linked" - so a
|
||||
// program that was told to be separable and then never linked is still NOT separable,
|
||||
// which is precisely what es31cSeparateShaderObjsTests's PipelineApi and CreateShadProgApi
|
||||
// assert. The live flag stays available as GetSeparable() for glGetProgramiv's sibling
|
||||
// state and for the next link to latch.
|
||||
Bool GetLinkedSeparable() const { return m_linkedSeparable; }
|
||||
void SetSeparable(Bool separable) {
|
||||
m_separable = separable;
|
||||
// ---- arming the uniform-write tracking latch ----
|
||||
@@ -1024,8 +1064,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint32 GetBlockBindingVersion() const { return m_blockBindingVersion; }
|
||||
|
||||
// Set by glUniformBlockBinding. The vector is seeded at link with each block's DECLARED
|
||||
// binding (layout(binding=N), else -1), so an untouched program already reports what its
|
||||
// shaders asked for.
|
||||
// binding (layout(binding=N)), and with GL's default of 0 for a block that declared none
|
||||
// - which the reflection cannot tell apart on its own, so the seeder consults
|
||||
// uniformBlocksWithoutBinding. Either way an untouched program already reports what GL
|
||||
// says it should.
|
||||
void SetUniformBlockBinding(Uint index, Uint binding) {
|
||||
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
||||
return;
|
||||
@@ -1287,8 +1329,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
@@ -1310,6 +1365,27 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
@@ -1337,6 +1413,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
@@ -1353,6 +1434,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||
// kept the built-in would read garbage - and recorded here rather than
|
||||
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||
// bytes no longer declare the capability that armed it. The backends read it to
|
||||
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
@@ -1460,6 +1553,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_requestedXfbVaryings = Move(names);
|
||||
m_requestedXfbBufferMode = bufferMode;
|
||||
}
|
||||
// NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
|
||||
// needs the capture list of the stage program it flattens, and the obvious source - what
|
||||
// glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
|
||||
// effect until the stage program's next link, and it bumps no version, so reading it makes
|
||||
// the composite's capture list depend on when the composite cache happened to be
|
||||
// invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
|
||||
// AND cache-safe, because linked state only moves at a link and the composite signature
|
||||
// already keys on the link version. See GLContext::GetProgramForDraw.
|
||||
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
|
||||
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
||||
@@ -1494,6 +1595,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
|
||||
// program has no geometry stage. Draws must present a compatible primitive type.
|
||||
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
|
||||
// GL_GEOMETRY_OUTPUT_TYPE (GL_POINTS, GL_LINE_STRIP or GL_TRIANGLE_STRIP),
|
||||
// GL_GEOMETRY_VERTICES_OUT and GL_GEOMETRY_SHADER_INVOCATIONS of the linked geometry
|
||||
// stage. Meaningless without one - glGetProgramiv raises INVALID_OPERATION there.
|
||||
GLenum GetGeometryOutputType() const { return Artifacts().gsOutputPrimitive; }
|
||||
Int GetGeometryVerticesOut() const { return Artifacts().gsMaxVertices; }
|
||||
Int GetGeometryShaderInvocations() const { return Artifacts().gsInvocations; }
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES of the linked tessellation control stage, or 0 when
|
||||
// the program has no such stage. Never greater than GL_MAX_PATCH_VERTICES: a program
|
||||
// that declared more does not link at all (GL 4.6 core 11.2.1.1).
|
||||
Int GetTessControlOutputVertices() const { return Artifacts().tcsOutputVertices; }
|
||||
// GL_TESS_GEN_MODE / _SPACING / _VERTEX_ORDER / _POINT_MODE of the linked tessellation
|
||||
// evaluation stage.
|
||||
GLenum GetTessGenMode() const { return Artifacts().tessGenMode; }
|
||||
GLenum GetTessGenSpacing() const { return Artifacts().tessGenSpacing; }
|
||||
GLenum GetTessGenVertexOrder() const { return Artifacts().tessGenVertexOrder; }
|
||||
Bool GetTessGenPointMode() const { return Artifacts().tessGenPointMode; }
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
||||
@@ -1619,6 +1736,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool m_deleteStatus = false;
|
||||
Bool m_binaryRetrievableHint = false;
|
||||
Bool m_separable = false;
|
||||
// m_separable as of the last link; see GetLinkedSeparable. Latched by Link() rather than
|
||||
// carried in LinkArtifacts because it is a GL-thread-owned decision made at enqueue time,
|
||||
// not a result the worker computes - and because a FAILED link still latches it, exactly
|
||||
// as a successful one does.
|
||||
Bool m_linkedSeparable = false;
|
||||
// Monotone "this program may ever be a pipeline stage" latch; see SetSeparable for why
|
||||
// it is a latch and not just m_separable. Outside LinkArtifacts on purpose: a relink
|
||||
// clears the write SET, but a program that was separable is still separable after it.
|
||||
|
||||
@@ -16,10 +16,29 @@
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
void ProgramSpirvTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
||||
namespace {
|
||||
// The MGLOG_*_ONCE latch, moved to the SOURCE of a deferred line. It cannot live at
|
||||
// the replay: Async::ApplyDeferredDiagnostics is ONE site shared by every job in the
|
||||
// tree, so a latch there would silence unrelated lines. And it has to exist: a shader
|
||||
// pack hands the same refusal to program after program, and a per-program WARN on a
|
||||
// path like that is exactly the repeated production logging the house rule forbids.
|
||||
// First occurrence at WARN - the one a bug report needs - every later one back at
|
||||
// DEBUG, which shipped builds compile out.
|
||||
Int FirstTimeWarnLevel(std::atomic_flag& latch) {
|
||||
return latch.test_and_set(std::memory_order_relaxed) ? MOBILEGL_LOG_LEVEL_DEBUG
|
||||
: MOBILEGL_LOG_LEVEL_WARN;
|
||||
}
|
||||
std::atomic_flag g_pointSizeDeclineReported;
|
||||
std::atomic_flag g_pointSizeOptimizerFailureReported;
|
||||
} // namespace
|
||||
|
||||
void ProgramSpirvTask::DeferLog(String line, const Int level) {
|
||||
diagnostics.logLines.push_back({level, Move(line)});
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
||||
@@ -128,8 +147,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
|
||||
// one answer could be handed back under the other.
|
||||
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
|
||||
// The point-size demotion verdicts, read from the SAME snapshot for the same reason
|
||||
// - and the same bits BuildSpirvCacheKey put in the L1 key, so a memo written under
|
||||
// one answer can never be handed back under the other.
|
||||
const Bool demoteTessellationPointSize =
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesTessellationPointSize();
|
||||
const Bool demoteGeometryPointSize =
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesGeometryPointSize();
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
||||
nativeFloat64);
|
||||
nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize);
|
||||
// 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
|
||||
@@ -188,7 +214,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation, const Bool nativeFloat64) {
|
||||
const Bool enableSpirvValidation, const Bool nativeFloat64,
|
||||
const Bool demoteTessellationPointSize,
|
||||
const Bool demoteGeometryPointSize) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -267,6 +295,50 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
artifacts.spirvStatus = allOptimized;
|
||||
|
||||
// The point-size demotion, program-wide and after the sanitize chain, so it works
|
||||
// on the final shared bytes both backends consume and nothing downstream can trim
|
||||
// the carriers it declares. Only the env half of the verdict lives here (and in the
|
||||
// L1 key); whether the program actually declares the capability is probed inside,
|
||||
// so the common case on an affected device - a program that never touches point
|
||||
// size in those stages - pays one module parse per stage and no rewrite.
|
||||
artifacts.pointSizeDemoted = false;
|
||||
if (allOptimized && (demoteTessellationPointSize || demoteGeometryPointSize)) {
|
||||
// Read off the HANDOFF's own derived bit, not off `handoff.reflection`: that
|
||||
// field is the routing slice phase A fills with eight named members, and
|
||||
// xfbVaryings is not one of them - reading it there answered "no capture ever
|
||||
// asks for gl_PointSize" on every production link, which left a read-only
|
||||
// capture stage without the carrier its capture binds to.
|
||||
const Bool captureRequestsPointSize = handoff.captureRequestsPointSize;
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
if (!ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
artifacts.generatedSpirv, handoff.shaderTypes, demoteTessellationPointSize,
|
||||
demoteGeometryPointSize, captureRequestsPointSize, outcome,
|
||||
!deferOutputValidationForDirectVulkan, enableSpirvValidation)) {
|
||||
// Optimizer failure: modules untouched, so the capability is still declared
|
||||
// and the backends' existing refusals stay in charge - honest, just slower.
|
||||
DeferLog(std::format("ProgramObject {}: point-size demotion failed in the optimizer; the "
|
||||
"program keeps its built-in and the device's declines apply",
|
||||
externalIndex),
|
||||
FirstTimeWarnLevel(g_pointSizeOptimizerFailureReported));
|
||||
} else if (outcome.demoted) {
|
||||
artifacts.pointSizeDemoted = true;
|
||||
DeferLog(std::format("ProgramObject {}: gl_PointSize demoted to an ordinary varying across "
|
||||
"the tessellation/geometry chain (value preserved for capture and "
|
||||
"gl_in reads; rasterized size falls back to 1.0)",
|
||||
externalIndex));
|
||||
} else if (!outcome.declineDetail.empty()) {
|
||||
// THE MOST VALUABLE LINE THIS FEATURE PRODUCES: which module shape the pass
|
||||
// refused, and therefore why an affected device is still about to lose the
|
||||
// program. Nothing else records it - `declineDetail` has no other runtime
|
||||
// surface - so at the deferred channel's DEBUG default it was formatted and
|
||||
// then dropped by every INFO build, i.e. every device and every CI artifact.
|
||||
DeferLog(std::format("ProgramObject {}: point-size demotion declined ({}); the program "
|
||||
"keeps its built-in and the device's declines apply",
|
||||
externalIndex, outcome.declineDetail),
|
||||
FirstTimeWarnLevel(g_pointSizeDeclineReported));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
||||
@@ -278,6 +350,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
artifacts.reservedNumSamplesOffset = ProgramObject::kInvalidUniformOffset;
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
@@ -311,6 +384,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
// The gl_NumSamples stand-in is routed by NAME and nothing else. It has no GL
|
||||
// location to look up - DoReflection hides it from the GL uniform index space
|
||||
// precisely so no application can address it - so the lookup below would find
|
||||
// nothing and log it as unbacked. Only the fragment stage declares it, and
|
||||
// every stage's copy sits at the same offset in the one shared global UBO.
|
||||
if (name == NUM_SAMPLES_UNIFORM_NAME) {
|
||||
artifacts.reservedNumSamplesOffset = offset;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - reserved gl_NumSamples stand-in '%s' "
|
||||
"backed at UBO offset %u",
|
||||
externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user