mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
70
Commits
01d20e5c96
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eae98581f | ||
|
|
d7655247f7 | ||
|
|
50fb13430f | ||
|
|
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 |
@@ -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: 7e25545174...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,13 +307,16 @@ 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
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.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
|
||||
|
||||
+104
-25
@@ -69,34 +69,34 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// 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 EnableGlesTextureView = false;
|
||||
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
|
||||
@@ -106,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
|
||||
@@ -119,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
|
||||
@@ -132,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
|
||||
@@ -151,15 +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;
|
||||
// MOBILEGL_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
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 DisableUnpackRing = false;
|
||||
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,
|
||||
@@ -180,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.
|
||||
@@ -224,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
|
||||
@@ -236,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
-15
@@ -162,34 +162,39 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EnableGlesTextureView = QueryEnvFlag("MOBILEGL_ENABLE_GLES_TEXTURE_VIEW");
|
||||
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.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_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");
|
||||
@@ -197,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() {
|
||||
|
||||
@@ -495,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;
|
||||
|
||||
@@ -1198,8 +1198,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// 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_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
|
||||
// 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
|
||||
@@ -1479,6 +1479,36 @@ 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;
|
||||
|
||||
@@ -393,6 +393,65 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// The capture points the CAPTURE PROGRAM uses, and nothing else.
|
||||
//
|
||||
// This used to go through SyncBufferBindingPoints, which walks the application's
|
||||
// GLOBAL touched-binding-point high-water mark and binds 0 to every point with no
|
||||
// frontend buffer. deqp/glcts permanently raises that mark to
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by clearing all of them after each test
|
||||
// case, so every capture using fewer points than that - i.e. every INTERLEAVED_ATTRIBS
|
||||
// capture - had glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) issued for the
|
||||
// unused tail immediately before glBeginTransformFeedback. The Mali G1-Ultra driver
|
||||
// then recorded NOTHING: no GL error, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0, the
|
||||
// application's buffer left holding its pre-draw bytes. Confirmed on device - the
|
||||
// separate/interleaved split in KHR-GL46.transform_feedback follows exactly whether
|
||||
// all four points were left bound.
|
||||
//
|
||||
// Those binds were never needed for correctness either. A capture only writes the
|
||||
// points the program's buffer mode uses (GL 4.6 core 13.2.2), so a point past
|
||||
// bufferCount cannot be written whatever is left bound there, and a point the program
|
||||
// DOES use with no buffer bound is already an error the frontend raised at
|
||||
// glBeginTransformFeedback. The rule this encodes: never issue a capture-point bind
|
||||
// the application did not ask for.
|
||||
//
|
||||
// Scoping it to the program (rather than skipping redundant binds behind the shadow)
|
||||
// is what makes it ORDER-INDEPENDENT: the shadow has to drop to unknown whenever a
|
||||
// transform feedback OBJECT is bound, since the points belong to the object, and the
|
||||
// clears came straight back for the next capture in the process.
|
||||
void SyncTransformFeedbackBindingPoints(SizeT bufferCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const SizeT pointCount = std::min<SizeT>(
|
||||
bufferCount, MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
|
||||
for (SizeT i = 0; i < pointCount; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
|
||||
const auto& obj = point.GetBoundObject();
|
||||
// A stride-0 slot (two consecutive gl_NextBuffer entries) captures nothing and
|
||||
// needs no binding; anything else with no buffer never got past the frontend.
|
||||
if (!obj) continue;
|
||||
|
||||
auto* backendResource = EnsureBufferResource(obj);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E_ONCE("No backend buffer for GL_TRANSFORM_FEEDBACK_BUFFER capture point %zu; the capture "
|
||||
"will not reach the application's buffer.",
|
||||
i);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& range = point.GetRange();
|
||||
const auto backendBufferId = backendResource->id;
|
||||
if (range.start == 0 && range.end >= obj->GetSize()) {
|
||||
BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId);
|
||||
} else {
|
||||
const auto start = std::min(range.start, obj->GetSize());
|
||||
const auto end = std::min(range.end, obj->GetSize());
|
||||
BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId,
|
||||
static_cast<GLintptr>(start), static_cast<GLsizeiptr>(end - start));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called once the storage-buffer points are bound and the draw/dispatch is about to
|
||||
// go out: whatever the shader writes there lands in the ES driver's buffers, behind
|
||||
// the frontend's CPU shadow. Flagging them makes the next MapBuffer/GetBufferSubData
|
||||
@@ -654,6 +713,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint backendId = 0;
|
||||
SizeT start = 0;
|
||||
SizeT end = 0;
|
||||
// WHICH capture buffer of the program this is. The list is COMPACTED - a
|
||||
// capture buffer with no bound buffer object contributes no entry - so the
|
||||
// position in the vector is not the program's buffer index, and everything
|
||||
// that asks the program about a target (its stride, which varyings land in
|
||||
// it) has to ask about this index instead. A capture list beginning with
|
||||
// gl_NextBuffer is the shape that makes them differ: buffer 0 has stride 0
|
||||
// and nothing bound, so target 0 describes buffer 1.
|
||||
SizeT bufferIndex = 0;
|
||||
};
|
||||
|
||||
// Per frontend transform feedback object. The default object (name 0) maps to
|
||||
@@ -698,6 +765,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return *g_currentXfbState;
|
||||
}
|
||||
|
||||
// EVERY way this path can lose a capture used to be silent: three unlogged early
|
||||
// returns before the driver Begin, an unchecked glBeginTransformFeedback, and two
|
||||
// `continue`s in the readback. The application sees a buffer that kept its
|
||||
// pre-draw bytes, GL_NO_ERROR, and GL_LINK_STATUS true - which is how one defect
|
||||
// reached ~320 conformance bodies across four families before anyone could say
|
||||
// which of the branches fired. Nothing below changes what MobileGL DOES on a
|
||||
// healthy capture; it only makes a lost one name itself in /sdcard/MG/latest.log.
|
||||
//
|
||||
// MGLOG_E_ONCE (not _D) on purpose: these have to be readable in an INFO-level
|
||||
// artifact, the same reason the backend link failure at Managers.cpp is MGLOG_E.
|
||||
constexpr Int kMaxDrainedXfbErrors = 32;
|
||||
|
||||
// The ES error raised by the call just issued, GL_NO_ERROR if it succeeded. Drains
|
||||
// the rest of the queue so the next probe cannot read this one as its own.
|
||||
GLenum TakeXfbDriverError() {
|
||||
const GLenum first = g_GLESFuncs.glGetError();
|
||||
if (first == GL_NO_ERROR) return GL_NO_ERROR;
|
||||
for (Int i = 0; i < kMaxDrainedXfbErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
Bool AreTransformFeedbackObjectsSupported() {
|
||||
return g_GLESFuncs.glGenTransformFeedbacks != nullptr &&
|
||||
g_GLESFuncs.glBindTransformFeedback != nullptr &&
|
||||
@@ -712,6 +801,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the backend already owns (coherent persistent map) need nothing: reads resolve
|
||||
// against that storage directly.
|
||||
void ReadbackCapturedRanges(Vector<XfbCaptureTarget>& targets) {
|
||||
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) {
|
||||
MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so "
|
||||
"captured data can never reach the application's buffers");
|
||||
}
|
||||
if (targets.empty()) {
|
||||
// The span closed with nothing to mirror back. Either the deferred Begin
|
||||
// never ran (a span with no draw - legal) or it ran and found no bound
|
||||
// capture buffer, which is not.
|
||||
MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets");
|
||||
}
|
||||
if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) {
|
||||
for (const auto& target : targets) {
|
||||
if (!target.buffer || target.buffer->IsBackendPersistentMapped()) continue;
|
||||
@@ -721,8 +820,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<GLintptr>(target.start),
|
||||
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u for capture readback",
|
||||
target.backendId);
|
||||
// Silent before: the capture landed in the ES buffer and the
|
||||
// application's next glMapBuffer read the untouched shadow, which
|
||||
// is indistinguishable from "the draw wrote nothing".
|
||||
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for "
|
||||
"capture readback (ES error %s); the captured data will NOT be visible to "
|
||||
"the application",
|
||||
target.backendId, target.start, target.end,
|
||||
MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str());
|
||||
continue;
|
||||
}
|
||||
target.buffer->WritebackFromBackend({mapped, size}, target.start);
|
||||
@@ -755,15 +860,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GL_DYNAMIC_COPY);
|
||||
g_scatterBufferSize = required;
|
||||
}
|
||||
// Point 0 carries every captured varying (the ES capture is INTERLEAVED); the
|
||||
// other points must be cleared or the driver would still write the app's buffers.
|
||||
// Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents
|
||||
// entries are consumed at link time and never reach the driver, so the ES
|
||||
// program is declared INTERLEAVED over a single buffer and point 0 is the only
|
||||
// point it can write (GL 4.6 core 13.2.2).
|
||||
//
|
||||
// The other points are therefore left exactly as they are. Clearing them - which
|
||||
// this used to do, across the application's whole touched high-water mark - is
|
||||
// both unnecessary (the ES program cannot write an unused point) and the precise
|
||||
// trigger for the Mali G1-Ultra capture loss: see
|
||||
// SyncTransformFeedbackBindingPoints for the mechanism and the device evidence.
|
||||
// KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that
|
||||
// reaches this path.
|
||||
BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0,
|
||||
static_cast<GLsizeiptr>(required));
|
||||
const SizeT pointCount =
|
||||
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::TransformFeedback);
|
||||
for (SizeT i = 1; i < pointCount; ++i) {
|
||||
BufferImpl::BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<Uint>(i), 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -777,10 +887,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return;
|
||||
|
||||
const SizeT packedStride = program->GetTransformFeedbackPackedStride();
|
||||
const SizeT vertices = std::min<SizeT>(
|
||||
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices()),
|
||||
xfb.scatterCapacityVertices);
|
||||
if (packedStride == 0 || vertices == 0) return;
|
||||
const SizeT modelledVertices =
|
||||
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());
|
||||
const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
|
||||
if (packedStride == 0 || vertices == 0) {
|
||||
// The scatter path redirected the DRIVER's capture into the scratch buffer,
|
||||
// so bailing here leaves the application's buffers holding their pre-draw
|
||||
// bytes - a total data loss, not a no-op. The vertex count is the CPU model
|
||||
// (AccountTransformFeedbackPrimitives), which is 0 for any draw mode
|
||||
// CountPrimitivesForDraw does not know and for the instanced/indirect entry
|
||||
// points that never call it.
|
||||
MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, "
|
||||
"CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep "
|
||||
"their pre-draw contents.",
|
||||
packedStride, modelledVertices, xfb.scatterCapacityVertices);
|
||||
return;
|
||||
}
|
||||
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
|
||||
const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0,
|
||||
@@ -798,14 +920,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) {
|
||||
const auto& target = xfb.targets[targetIndex];
|
||||
if (!target.buffer) continue;
|
||||
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(targetIndex));
|
||||
// By BUFFER index, not by position in the compacted list - see XfbCaptureTarget.
|
||||
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
|
||||
if (stride == 0) continue;
|
||||
const SizeT rangeBytes = target.end - target.start;
|
||||
Vector<Uint8> staged(rangeBytes);
|
||||
Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes);
|
||||
|
||||
for (const auto& varying : program->GetTransformFeedbackVaryings()) {
|
||||
if (varying.bufferIndex != targetIndex) continue;
|
||||
if (varying.bufferIndex != target.bufferIndex) continue;
|
||||
for (SizeT v = 0; v < vertices; ++v) {
|
||||
const SizeT dstOffset = v * stride + varying.offsetBytes;
|
||||
if (dstOffset + varying.byteSize > rangeBytes) break;
|
||||
@@ -860,9 +983,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// not captured, and opening the span would also subject it to the capture
|
||||
// primitive-mode rule the paused draw is exempt from.
|
||||
if (!xfb.pending || xfb.paused) return;
|
||||
xfb.pending = false;
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
if (!program) return;
|
||||
if (!program) {
|
||||
// The pending flag is deliberately NOT consumed here. It used to be cleared
|
||||
// before this check, so a single draw that could not see the capture program
|
||||
// retired the span permanently: every later draw of the same span found
|
||||
// pending==false, the driver Begin never happened, and End found started==false
|
||||
// and skipped the readback - a whole capture lost with no GL error anywhere.
|
||||
// The frontend only reaches a draw with an active span after glBeginTransformFeedback
|
||||
// stored a program, so this is a "cannot happen" that must stay recoverable.
|
||||
MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the "
|
||||
"driver span stays closed and this draw is not captured");
|
||||
return;
|
||||
}
|
||||
xfb.pending = false;
|
||||
|
||||
// Snapshot what the driver is about to capture into. GL forbids rebinding the
|
||||
// capture buffers while the span is open, so this stays valid until End, and
|
||||
@@ -879,10 +1013,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT start = std::min(range.start, bufferObject->GetSize());
|
||||
const SizeT end = std::min(range.end, bufferObject->GetSize());
|
||||
if (end <= start) continue;
|
||||
xfb.targets.push_back({bufferObject, backendResource->id, start, end});
|
||||
xfb.targets.push_back({bufferObject, backendResource->id, start, end, i});
|
||||
}
|
||||
|
||||
BufferImpl::SyncBufferBindingPoints(BufferTarget::TransformFeedback, GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||
BufferImpl::SyncTransformFeedbackBindingPoints(bufferCount);
|
||||
|
||||
// A layout with holes or several interleaved buffers is not expressible on ES:
|
||||
// capture gap-free into scratch storage and place the records at End instead.
|
||||
@@ -891,31 +1025,99 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
xfb.scatterCapacityVertices = 0;
|
||||
if (program->NeedsScatteredTransformFeedbackCapture()) {
|
||||
SizeT capacityVertices = ~SizeT(0);
|
||||
for (SizeT i = 0; i < xfb.targets.size(); ++i) {
|
||||
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
for (const auto& target : xfb.targets) {
|
||||
// By BUFFER index. Reading the stride at the target's POSITION made a
|
||||
// capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and
|
||||
// nothing bound, so target 0 describes buffer 1 - read stride 0, skip every
|
||||
// target, and leave the capacity at zero.
|
||||
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
|
||||
if (stride == 0) continue;
|
||||
capacityVertices =
|
||||
std::min<SizeT>(capacityVertices, (xfb.targets[i].end - xfb.targets[i].start) / stride);
|
||||
capacityVertices = std::min<SizeT>(capacityVertices, (target.end - target.start) / stride);
|
||||
}
|
||||
if (capacityVertices == ~SizeT(0)) capacityVertices = 0;
|
||||
if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) {
|
||||
xfb.scattered = true;
|
||||
xfb.scatterProgram = program;
|
||||
xfb.scatterCapacityVertices = capacityVertices;
|
||||
} else {
|
||||
// NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a
|
||||
// scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer /
|
||||
// gl_SkipComponents entries are consumed at link time and never reach the
|
||||
// driver), so it writes capture point 0 and nothing else. Point 0 here is
|
||||
// either unbound or - the dangerous case - still holds whatever an earlier
|
||||
// capture in this process bound there, because the frontend's own
|
||||
// glBindBufferBase is state-only and nothing else in the backend touches the
|
||||
// indexed points. Opening the span would then have the driver capture over an
|
||||
// application buffer that has nothing to do with this draw, and the frontend
|
||||
// shadow would never learn of it.
|
||||
//
|
||||
// Leaving the span closed reproduces exactly what the old high-water clear
|
||||
// loop achieved by binding 0 here and letting the driver refuse the Begin -
|
||||
// the capture records nothing - without issuing a capture-point bind the
|
||||
// application did not ask for, which is the thing that loses captures whole
|
||||
// on Mali (see SyncTransformFeedbackBindingPoints).
|
||||
MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture "
|
||||
"(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the "
|
||||
"capture cannot land in a stale binding. Nothing will be captured.",
|
||||
program->GetTransformFeedbackPackedStride(), capacityVertices);
|
||||
xfb.targets.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A capture program with buffers bound must have produced at least one target;
|
||||
// an empty list means End has nothing to mirror back and the application will
|
||||
// read its buffer's pre-draw bytes however well the GPU captured.
|
||||
if (xfb.targets.empty()) {
|
||||
MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets "
|
||||
"(program declares %zu capture buffer(s), none of them resolved to a bound backend "
|
||||
"buffer with a non-empty range); nothing will be read back",
|
||||
bufferCount);
|
||||
}
|
||||
|
||||
g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode);
|
||||
// Unchecked before. Every ES error condition here (already active, a current
|
||||
// program with no capture set, a capture point the program uses with no buffer)
|
||||
// ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
// reads 0 and the application sees no error at all - MobileGL's own error state is
|
||||
// separate from the driver's, so a driver rejection here is invisible to it.
|
||||
if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) {
|
||||
// The mode is printed as a number as well as a name: GL_POINTS is 0, which the
|
||||
// enum converter spells "GL_FALSE", and a reader chasing a lost capture should
|
||||
// not have to know that.
|
||||
MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED "
|
||||
"glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend "
|
||||
"program %u, %zu capture buffer(s), %zu target(s), mode=%s.",
|
||||
MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(),
|
||||
static_cast<unsigned>(xfb.primitiveMode),
|
||||
MG_Util::ConvertGLEnumToString(beginError).c_str(),
|
||||
PrgramImpl::g_lastUsedBackendProgramId, bufferCount,
|
||||
xfb.targets.size(),
|
||||
MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str());
|
||||
}
|
||||
xfb.started = true;
|
||||
}
|
||||
|
||||
void EndTransformFeedback() {
|
||||
auto& xfb = CurrentXfb();
|
||||
const Bool wasPending = xfb.pending;
|
||||
xfb.pending = false;
|
||||
xfb.paused = false;
|
||||
if (!xfb.started) return;
|
||||
if (!xfb.started) {
|
||||
// A span that never drew is legal and captures nothing by definition; one that
|
||||
// is STILL pending here drew nothing the backend saw, which for a span the
|
||||
// application expected data from is the whole bug in one line.
|
||||
MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)",
|
||||
wasPending ? 1 : 0);
|
||||
return;
|
||||
}
|
||||
xfb.started = false;
|
||||
g_GLESFuncs.glEndTransformFeedback();
|
||||
if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the "
|
||||
"driver's capture state and MobileGL's have diverged",
|
||||
MG_Util::ConvertGLEnumToString(endError).c_str());
|
||||
}
|
||||
if (xfb.scattered) {
|
||||
ScatterCapturedRecords(xfb);
|
||||
xfb.scattered = false;
|
||||
@@ -943,6 +1145,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BindTransformFeedback(GLuint name) {
|
||||
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
|
||||
// The capture buffer bindings are the OBJECT's, not the context's: the bind below
|
||||
// swaps all of them for whatever the target object holds, which the redundant-bind
|
||||
// shadow has never seen.
|
||||
BufferImpl::InvalidateTransformFeedbackBindingShadows();
|
||||
if (!AreTransformFeedbackObjectsSupported()) {
|
||||
// Without driver objects there is only the default span; keep the frontend
|
||||
// name so the bookkeeping below stays consistent.
|
||||
@@ -979,6 +1185,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_currentXfbName = 0;
|
||||
g_scatterBufferId = 0;
|
||||
g_scatterBufferSize = 0;
|
||||
BufferImpl::InvalidateTransformFeedbackBindingShadows();
|
||||
}
|
||||
} // namespace XfbImpl
|
||||
|
||||
@@ -1772,6 +1979,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
|
||||
// the clear's undoctored mask.
|
||||
static Uint32 g_syncedColorMaskAlphaWidenMask = 0;
|
||||
// Scratch for the dual-source-blend decline path in the blend block below. File-scope
|
||||
// rather than a local so the ordinary draw pays nothing for it: it is written only on a
|
||||
// driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and
|
||||
// SyncRenderState runs on the GL thread only.
|
||||
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
|
||||
g_dualSourceDeclinedBlendStates;
|
||||
void InvalidateSyncedRenderState() {
|
||||
g_forceFullRenderStateResync = true;
|
||||
g_hasSyncedRenderState = false;
|
||||
@@ -1931,31 +2144,85 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; };
|
||||
|
||||
// Which draw buffers the blend block below DECLINED (see it for why). Needed again at
|
||||
// the shadow write-back at the end of this function: the span memcpy there clones the
|
||||
// FRONTEND block, which for a declined draw buffer is not what the driver was handed.
|
||||
Uint32 dualSourceDeclinedMask = 0;
|
||||
|
||||
if (blendSpanDirty) { // Blend State
|
||||
using FBO = MG_State::GLState::FramebufferObject;
|
||||
const auto& targetStates = parameters.BlendStates;
|
||||
auto& syncedStates = g_syncedRenderStateParameters.BlendStates;
|
||||
|
||||
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
||||
// glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none.
|
||||
// Detected at load and surfaced in the POST. There is no fallback, so if a draw actually
|
||||
// enables blending with a SRC1 factor on a driver that lacks it, hard-fail here at use
|
||||
// time rather than let the driver reject glBlendFuncSeparate and silently mis-blend.
|
||||
// Detected at load and surfaced in the POST. There is no fallback that BLENDS
|
||||
// correctly, so a draw that asks for a SRC1 factor on a driver without the extension
|
||||
// gets the blend DECLINED: that draw buffer is pushed with blending off and neutral
|
||||
// One/Zero factors, and the loss is logged once. The two rejected alternatives are
|
||||
// both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise
|
||||
// GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend
|
||||
// against stale state), and throwing, which is what this did until now, takes the
|
||||
// whole process down over one unsupported blend factor. Declining is defined,
|
||||
// survivable and visible in the log.
|
||||
//
|
||||
// NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated
|
||||
// on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR
|
||||
// ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate
|
||||
// serves every draw buffer when they agree, and the per-index arm diffs factors
|
||||
// alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any
|
||||
// draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers
|
||||
// GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal
|
||||
// no-error probe to read as its own, and leaving this shadow recording factors the
|
||||
// ES context rejected. Blending being off makes the picture unaffected; it does not
|
||||
// make the enum acceptable.
|
||||
const auto* effectiveBlendStates = ¶meters.BlendStates;
|
||||
if (!g_GLESCapabilities.SupportsDualSourceBlend) {
|
||||
Uint32 declinedWithBlendingOnMask = 0;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
const auto& s = targetStates[i];
|
||||
if (s.Enabled &&
|
||||
(IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
|
||||
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha))) {
|
||||
THROW_EXCEPTION(
|
||||
"Dual-source blending (GL_SRC1_* blend factor) was used on draw buffer " +
|
||||
std::to_string(i) +
|
||||
", but the GLES driver does not expose GL_EXT_blend_func_extended (see the "
|
||||
"dual-source blend row in the driver POST). No fallback exists; the draw "
|
||||
"cannot proceed.");
|
||||
const auto& s = parameters.BlendStates[i];
|
||||
if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
|
||||
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) {
|
||||
dualSourceDeclinedMask |= 1u << i;
|
||||
if (s.Enabled) declinedWithBlendingOnMask |= 1u << i;
|
||||
}
|
||||
}
|
||||
if (dualSourceDeclinedMask != 0) {
|
||||
// Two masks in the message because they mean different things to whoever
|
||||
// reads the log: the second one is where a PICTURE was lost. A draw buffer
|
||||
// in the first mask but not the second had blending off anyway, so nothing
|
||||
// was blended and nothing was dropped - only the unusable enum was kept out
|
||||
// of the driver.
|
||||
MGLOG_E_ONCE(
|
||||
"SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer "
|
||||
"mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see "
|
||||
"the dual-source blend row in the driver POST). Those draw buffers are pushed "
|
||||
"with neutral One/Zero factors instead. Blending was actually ENABLED on mask "
|
||||
"0x%x, and only there is anything lost: the fragment's first output is written "
|
||||
"unblended and the second source is dropped.",
|
||||
dualSourceDeclinedMask, declinedWithBlendingOnMask);
|
||||
g_dualSourceDeclinedBlendStates = parameters.BlendStates;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
||||
auto& s = g_dualSourceDeclinedBlendStates[i];
|
||||
// Both halves, for the same reason the Vulkan arm neutralises both: the
|
||||
// enable so nothing blends against a source the driver cannot produce,
|
||||
// the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled
|
||||
// on a buffer that was already off is a no-op, which is what makes one
|
||||
// ungated rule serve both cases.
|
||||
s.Enabled = false;
|
||||
s.SrcFactorRGB = BlendFactor::One;
|
||||
s.DstFactorRGB = BlendFactor::Zero;
|
||||
s.SrcFactorAlpha = BlendFactor::One;
|
||||
s.DstFactorAlpha = BlendFactor::Zero;
|
||||
}
|
||||
effectiveBlendStates = &g_dualSourceDeclinedBlendStates;
|
||||
}
|
||||
}
|
||||
// The rest of the block reads the EFFECTIVE state. The per-field writes it makes
|
||||
// into `syncedStates` are provisional - the span memcpy at the end of this function
|
||||
// overwrites the whole blend span with the frontend's own bytes - so the declined
|
||||
// draw buffers are put back there, see the write-back below.
|
||||
const auto& targetStates = *effectiveBlendStates;
|
||||
|
||||
Bool allEnabled = true;
|
||||
Bool allDisabled = true;
|
||||
@@ -2355,6 +2622,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (blendSpanDirty) {
|
||||
std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin,
|
||||
kBlendSpanEnd - kBlendSpanBegin);
|
||||
// ...except for a draw buffer whose dual-source blend was DECLINED, where the
|
||||
// frontend block is precisely what did NOT reach the driver. The shadow has to hold
|
||||
// what was pushed or the next diff compares against state the ES context never got:
|
||||
// going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides,
|
||||
// the enable block finds nothing to do, and blending stays off from the decline.
|
||||
// The span stays permanently "dirty" against the frontend as a result, which costs
|
||||
// one memcmp plus this block per render-state VERSION change - the top-of-function
|
||||
// version early-out still skips repeat draws entirely.
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
||||
g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i];
|
||||
}
|
||||
}
|
||||
if (tailSpanDirty) {
|
||||
std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd,
|
||||
@@ -10357,6 +10636,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// frame's ring high-water marks for slot reclamation.
|
||||
BufferImpl::UboRingOnPresent();
|
||||
BufferImpl::UnpackRingOnPresent();
|
||||
BufferImpl::UploadRingOnPresent();
|
||||
BufferImpl::TrimBufferPool();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the driver fifteen more rectangles to rasterize against, and nothing in MobileGL has ever
|
||||
// programmed the indexed state it would need.
|
||||
Bool ViewportArrayEmulationEnabled() {
|
||||
return MG_Config::Features.ViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
|
||||
return MG_Config::Features.EsprytViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
Bool g_anyProgramRoutesViewportIndex = false;
|
||||
@@ -75,13 +75,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() {
|
||||
// IsAngleLlvmpipeRenderer combined with the
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle,
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle,
|
||||
// both resolved in FillInGLESCapabilities.
|
||||
return g_GLESCapabilities.AvoidSamplerMipmapMinFilter;
|
||||
}
|
||||
|
||||
static Bool ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() {
|
||||
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_AVOID_EXPLICIT_LOD_BIAS
|
||||
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS
|
||||
// feature toggle, both resolved in FillInGLESCapabilities.
|
||||
return g_GLESCapabilities.AvoidExplicitLodBias;
|
||||
}
|
||||
@@ -648,6 +648,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to
|
||||
// spare and keeps consecutive staged blocks off each other's cache lines.
|
||||
constexpr SizeT kUnpackRingAlignment = 64;
|
||||
// glCopyBufferSubData carries no offset-alignment requirement at all; 64
|
||||
// keeps staged blocks cache-line separated, same as the unpack ring.
|
||||
constexpr SizeT kUploadRingInitialBytes = 4u * 1024u * 1024u;
|
||||
constexpr SizeT kUploadRingMaxBytes = 64u * 1024u * 1024u;
|
||||
constexpr SizeT kUploadRingAlignment = 64;
|
||||
|
||||
struct PersistentRingStore {
|
||||
Uint id = 0;
|
||||
@@ -705,6 +710,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
kUnpackRingMaxBytes,
|
||||
kUnpackRingAlignment,
|
||||
"Texture unpack ring"};
|
||||
// Staging ring for app buffer updates whose destination store may still be
|
||||
// referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR
|
||||
// hazard by BLOCKING in the call (osup_sync_object_wait) until every
|
||||
// referencing job retires - under Minecraft 26.3's per-frame UBO and
|
||||
// chunk-mesh SubData streams that serialized whole frames (~1 fps while
|
||||
// chunks stream in). Staging the bytes here and issuing a
|
||||
// glCopyBufferSubData instead keeps the hazard on the GPU timeline where it
|
||||
// is just job ordering, and the CPU never waits.
|
||||
PersistentRing g_uploadRing{{},
|
||||
{},
|
||||
{},
|
||||
kUploadRingInitialBytes,
|
||||
kUploadRingMaxBytes,
|
||||
kUploadRingAlignment,
|
||||
"Buffer upload ring"};
|
||||
|
||||
// The ES context the ring's id/map belonged to is gone (or was never
|
||||
// seen): drop every handle without GL calls and re-arm creation. The
|
||||
@@ -763,6 +783,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource.storageInitialized = true;
|
||||
resource.pendingRespecify = false;
|
||||
resource.pendingRanges.clear();
|
||||
resource.pendingResidentWrites.clear();
|
||||
resource.syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
|
||||
// rebind whenever the shadow already records this id at that index - so on a
|
||||
@@ -794,6 +815,161 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
bufferObject.MappedData() + start);
|
||||
}
|
||||
|
||||
// Ring machinery shared with the UBO/unpack rings; defined further down in
|
||||
// this same unnamed namespace.
|
||||
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset);
|
||||
Bool RingAvailable(PersistentRing& ring);
|
||||
|
||||
// True when a pending-range flush can go through the staging ring right
|
||||
// now: kill switch off, the ES copy entry point resolved, and the ring's
|
||||
// own availability gate (EXT_buffer_storage + fences + live context) up.
|
||||
Bool UploadRingUsableNow() {
|
||||
if (MG_Config::Features.EsprytDisableUploadRing) return false;
|
||||
if (!g_GLESFuncs.glCopyBufferSubData) return false;
|
||||
return RingAvailable(g_uploadRing);
|
||||
}
|
||||
|
||||
// A partial range below this goes through the staging ring instead of a
|
||||
// range-invalidating map: the map's page-substitution fast path needs a
|
||||
// sizeable (page-coverable) range to engage, and below it the driver
|
||||
// falls back to waiting out the WAR hazard on the CPU.
|
||||
constexpr SizeT kInvalidateRangeMinBytes = 128u * 1024u;
|
||||
|
||||
// Push every queued range of `resource` from the shadow into the backend
|
||||
// store, without ever letting a driver resolve the WAR hazard against
|
||||
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
|
||||
//
|
||||
// 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire
|
||||
// mapped range is rewritten from the authoritative shadow, so
|
||||
// declaring its old bytes dead is exact - and it lets the driver
|
||||
// swap fresh pages in for JUST that range. This is the only tier
|
||||
// whose cost scales with the RANGE on this Mali driver: both the
|
||||
// immediate glBufferSubData (pre-queueing) and a staged
|
||||
// glCopyBufferSubData into a busy MUTABLE store ghost the whole
|
||||
// destination with a worker-thread memcpy - Minecraft 26.3 streams
|
||||
// ~1MB section meshes into 128MB arenas about nine times a frame
|
||||
// during a camera pan, and 9 x 128MB of ghosting per frame is
|
||||
// ~380ms, the measured 2-4 fps. (Backing the arenas with immutable
|
||||
// stores also kills the ghost, but eagerly commits every arena's
|
||||
// full extent - +hundreds of MB - which LMK'd the whole device.)
|
||||
// 2. The staging ring + glCopyBufferSubData: the copy is ordered on
|
||||
// the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH
|
||||
// forces this tier as the map path's negative control).
|
||||
// 3. Direct glBufferSubData (potentially stalling) when neither the
|
||||
// map entry points nor the ring exist.
|
||||
//
|
||||
// The ranges are flushed AS QUEUED (VecRange1D::Add already merges
|
||||
// near-adjacent ones): bytes, not flush calls, are the cost axis here,
|
||||
// and collapsing a scattered flush into its union re-copied nearly whole
|
||||
// chunk-mesh arenas every frame.
|
||||
// The caller owns syncedChangeSerial; this only drains the queue.
|
||||
void FlushPendingRangesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
VecRange1D ranges;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
if (resource.pendingRanges.empty()) return;
|
||||
ranges = std::move(resource.pendingRanges);
|
||||
resource.pendingRanges.clear();
|
||||
}
|
||||
// Clamp against BOTH extents: the readback flush may run while the
|
||||
// frontend size and the backend store disagree (a pending respecify
|
||||
// resolves that later; bytes past either end have nowhere to land).
|
||||
const SizeT limit = std::min(bufferObject.GetSize(), resource.storageSize);
|
||||
const Bool mapUsable = !MG_Config::Features.EsprytDisableInvalidateFlush &&
|
||||
g_GLESFuncs.glMapBufferRange && g_GLESFuncs.glUnmapBuffer;
|
||||
const Bool ringUsable = UploadRingUsableNow();
|
||||
for (const auto& range : ranges) {
|
||||
const SizeT end = std::min(range.end, limit);
|
||||
const SizeT start = std::min(range.start, end);
|
||||
const SizeT size = end - start;
|
||||
if (size == 0) continue;
|
||||
// The invalidating map's fast path is SHAPE-dependent on this Mali
|
||||
// driver: a whole-buffer invalidation renames the store outright,
|
||||
// and a large range gets fresh pages - but a small unaligned range
|
||||
// of a busy store makes the map WAIT (osup_sync_object_wait, ~9%
|
||||
// of a Minecraft 26.3 replay). So: whole buffer -> orphan-map;
|
||||
// large range -> range-invalidating map; small range -> the staged
|
||||
// ring copy, whose worst case (a whole-destination ghost) is only
|
||||
// ever the small destination itself.
|
||||
//
|
||||
// The map covers EXACTLY the queued range: only those bytes are the
|
||||
// shadow's to rewrite. Widening to page bounds looked free and was
|
||||
// not - the widened bytes clobbered GPU-written data (an SSBO
|
||||
// counter beside the app's SubData) with the stale shadow.
|
||||
const Bool wholeBuffer = start == 0 && end == limit && limit == resource.storageSize;
|
||||
if (mapUsable && (wholeBuffer || size >= kInvalidateRangeMinBytes)) {
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
const GLbitfield access =
|
||||
GL_MAP_WRITE_BIT |
|
||||
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
|
||||
void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start,
|
||||
(GLsizeiptr)size, access);
|
||||
if (dst) {
|
||||
Memcpy(dst, bufferObject.MappedData() + start, size);
|
||||
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
|
||||
} else {
|
||||
UploadRangeNow(resource, bufferObject, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Land the app bytes queued for an ADOPTED store on the GPU timeline: staged
|
||||
// into the upload ring and delivered by glCopyBufferSubData. The destination
|
||||
// is the IMMUTABLE persistent store, which the driver can neither rename nor
|
||||
// ghost, so the copy is plain job ordering - after every in-flight reader,
|
||||
// before the next consumer - which is exactly glBufferSubData's contract.
|
||||
// (The in-place host write these bytes replaced tore the frames still
|
||||
// reading the old vertex data: one-frame wrong geometry during fast camera
|
||||
// movement.) Fallback: direct glBufferSubData - the adopted store carries
|
||||
// DYNAMIC_STORAGE, and immutability again forbids the whole-store ghost.
|
||||
void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
Vector<GLESBufferResource::PendingResidentWrite> writes;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
if (resource.pendingResidentWrites.empty()) return;
|
||||
writes = std::move(resource.pendingResidentWrites);
|
||||
resource.pendingResidentWrites.clear();
|
||||
}
|
||||
const SizeT limit = resource.storageSize;
|
||||
const Bool ringUsable = UploadRingUsableNow();
|
||||
for (const auto& write : writes) {
|
||||
if (write.offset >= limit) continue;
|
||||
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
|
||||
if (size == 0) continue;
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)write.offset,
|
||||
(GLsizeiptr)size);
|
||||
} else {
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)write.offset, (GLsizeiptr)size,
|
||||
write.bytes.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
||||
// tokens); defined locally so this compiles regardless of which GLES headers
|
||||
// expose the EXT tokens.
|
||||
@@ -879,6 +1055,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
}
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return ptr;
|
||||
@@ -904,6 +1081,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (resource->id != 0 && CanTouchGLNow() &&
|
||||
resource->contextGeneration == g_bufferContextGeneration) {
|
||||
NoteBufferIdDeleted(resource->id);
|
||||
// Frontend VAO bindings survive respecification; force their
|
||||
// backend twins to bind the replacement buffer name.
|
||||
++g_bufferBackendIdGeneration;
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
resource->immutableStorage = false;
|
||||
@@ -916,12 +1096,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->storageSize = 0;
|
||||
resource->pendingRespecify = true;
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
return;
|
||||
}
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
resource->contextGeneration != g_bufferContextGeneration) {
|
||||
resource->pendingRespecify = true;
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
return;
|
||||
}
|
||||
if (bufferObject.GetSize() == 0) {
|
||||
@@ -929,6 +1111,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->storageSize = 0;
|
||||
resource->pendingRespecify = false;
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
return;
|
||||
}
|
||||
RespecifyStorageNow(*resource, bufferObject);
|
||||
@@ -941,11 +1124,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
resource->contextGeneration != g_bufferContextGeneration ||
|
||||
!StorageMatches(*resource, bufferObject)) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.Add({offset, offset + size});
|
||||
return;
|
||||
}
|
||||
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
// An adopted zero-copy persistent store already HAS the bytes (the
|
||||
// frontend wrote them through the coherent mapping); a driver upload
|
||||
// here would be a self-copy that re-synchronizes what coherent mapping
|
||||
// made free.
|
||||
if (resource->persistentMapped && resource->persistentPtr) {
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
// An immediate glBufferSubData resolves the WAR hazard against frames
|
||||
// still referencing this store on the CPU on some drivers - Mali parks
|
||||
// the thread in osup_sync_object_wait until every referencing job
|
||||
// retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh
|
||||
// update streams into ~1 fps. Queue the range instead (the shadow
|
||||
// already holds the bytes) and let draw-time sync push the merged
|
||||
// ranges through the staging ring.
|
||||
if (MG_Config::Features.EsprytDisableUploadRing) {
|
||||
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.Add({offset, offset + size});
|
||||
}
|
||||
|
||||
// App bytes for an ADOPTED store: queue them untouched-by-the-mapping; the
|
||||
// draw-time sync (or a readback) lands them GPU-ordered through
|
||||
// DrainResidentWritesNow. No GL here, so the op is thread-agnostic.
|
||||
void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) {
|
||||
auto* resource = ResourceOf(bufferObject);
|
||||
if (!resource || data.size == 0) return;
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
auto& write = resource->pendingResidentWrites.emplace_back();
|
||||
write.offset = offset;
|
||||
const auto* bytes = static_cast<const Uint8*>(data.data);
|
||||
write.bytes.assign(bytes, bytes + data.size);
|
||||
}
|
||||
|
||||
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
||||
@@ -956,6 +1173,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
resource->contextGeneration != g_bufferContextGeneration ||
|
||||
!StorageMatches(*resource, bufferObject)) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.Add(range);
|
||||
return;
|
||||
}
|
||||
|
||||
// An adopted zero-copy persistent store already HAS the bytes: the
|
||||
// frontend shadow IS the coherent mapping the app (or UploadSubData)
|
||||
// wrote into, so publishing is free. The self-copy that used to run
|
||||
// here mapped a buffer this backend keeps persistently mapped (an
|
||||
// INVALID_OPERATION whose fallback was a WAR-stalling
|
||||
// glBufferSubData).
|
||||
if (resource->persistentMapped && resource->persistentPtr) {
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
|
||||
// Same WAR-hazard rule as Ops_SubData: an immediate synchronized upload
|
||||
// (mapped or glBufferSubData) can park the thread on Mali until the
|
||||
// frames still referencing this store retire. Queue the range for the
|
||||
// staged flush at draw-time sync; the negative-control kill switch
|
||||
// keeps the immediate paths below.
|
||||
if (!MG_Config::Features.EsprytDisableUploadRing) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.Add(range);
|
||||
return;
|
||||
}
|
||||
@@ -998,8 +1238,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||
if (resource->persistentMapped) {
|
||||
// Host writes to a persistent map must not race shader writes already queued
|
||||
// on this context. There is no backend copy to read back in this case.
|
||||
// Queued resident SubData bytes land first (GPU-ordered), then the
|
||||
// finish makes them - and any shader writes already queued on this
|
||||
// context - visible through the coherent mapping the reads use.
|
||||
// There is no backend copy to read back in this case.
|
||||
DrainResidentWritesNow(*resource, bufferObject);
|
||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||
return;
|
||||
}
|
||||
@@ -1007,6 +1250,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
||||
if (size == 0) return;
|
||||
|
||||
// Queued app writes must land in the backend store before it is read
|
||||
// back, or the writeback below would revert them in the shadow.
|
||||
FlushPendingRangesNow(*resource, bufferObject);
|
||||
|
||||
BindBufferId(TempBufferTarget, resource->id);
|
||||
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||
GL_MAP_READ_BIT);
|
||||
@@ -1061,6 +1308,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Ops_SubData(bufferObject, offset, size);
|
||||
BumpBufferMutationEpoch();
|
||||
}
|
||||
void Ops_ResidentSubDataTracked(BufferObject& bufferObject, SizeT offset, DataPtr data) {
|
||||
Ops_ResidentSubData(bufferObject, offset, data);
|
||||
BumpBufferMutationEpoch();
|
||||
}
|
||||
void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> appAccess) {
|
||||
Ops_FlushMappedRange(bufferObject, range, appAccess);
|
||||
@@ -1085,6 +1336,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const BufferBackendOps g_glesBufferBackendOps = {
|
||||
.Respecify = Ops_RespecifyTracked,
|
||||
.SubData = Ops_SubDataTracked,
|
||||
.ResidentSubData = Ops_ResidentSubDataTracked,
|
||||
.FlushMappedRange = Ops_FlushMappedRangeTracked,
|
||||
.OnDestroy = Ops_OnDestroyTracked,
|
||||
.AcquirePersistentMap = Ops_AcquirePersistentMapTracked,
|
||||
@@ -1101,7 +1353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
// See the declaration: re-mints of a live resource's driver id. Written only on
|
||||
// the context thread (both re-mint sites run there), read only by the VAO sync.
|
||||
// the context thread (all re-mint sites run there), read only by the VAO sync.
|
||||
Uint64 g_bufferBackendIdGeneration = 0;
|
||||
|
||||
void RegisterBufferBackendOps() {
|
||||
@@ -1186,7 +1438,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (resource->id == 0) return false;
|
||||
// Zero-copy coherent persistent store: EnsureBufferResource's own early-out —
|
||||
// the app writes straight into the mapped GPU storage, nothing to sync.
|
||||
if (resource->persistentMapped) return resource->persistentPtr != nullptr;
|
||||
// Except queued resident SubData bytes, which land through the sync path
|
||||
// (same unlocked emptiness probe as pendingRanges below).
|
||||
if (resource->persistentMapped) {
|
||||
return resource->persistentPtr != nullptr && resource->pendingResidentWrites.empty();
|
||||
}
|
||||
// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push
|
||||
// (persistent maps mutate the shadow without bumping the change serial).
|
||||
if (frontend->IsMapped()) return false;
|
||||
@@ -1218,6 +1474,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->storageSize = 0;
|
||||
resource->pendingRespecify = true;
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
resource->contextGeneration = g_bufferContextGeneration;
|
||||
// The persistent map (and its pointer) died with the old context; the
|
||||
// frontend re-acquires a fresh one on its next map.
|
||||
@@ -1247,6 +1504,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// persistently mapped immutable store, so there is nothing to (re)upload at
|
||||
// draw time. This is where the per-draw whole-buffer glBufferSubData used to run.
|
||||
if (resource->persistentMapped && resource->persistentPtr && resource->id != 0) {
|
||||
DrainResidentWritesNow(*resource, *bufferObject);
|
||||
return resource;
|
||||
}
|
||||
|
||||
@@ -1268,6 +1526,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.clear();
|
||||
resource->pendingResidentWrites.clear();
|
||||
}
|
||||
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
||||
} else {
|
||||
@@ -1295,11 +1554,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->storageSize != bufferObject->GetSize()) {
|
||||
RespecifyStorageNow(*resource, *bufferObject);
|
||||
} else if (!resource->pendingRanges.empty()) {
|
||||
for (const auto& range : resource->pendingRanges) {
|
||||
const SizeT end = std::min(range.end, bufferObject->GetSize());
|
||||
UploadRangeNow(*resource, *bufferObject, std::min(range.start, end), end);
|
||||
}
|
||||
resource->pendingRanges.clear();
|
||||
FlushPendingRangesNow(*resource, *bufferObject);
|
||||
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
||||
} else if (resource->syncedChangeSerial != bufferObject->GetChangeSerial()) {
|
||||
// Ops could not track some writes (e.g. the ops table was
|
||||
@@ -1383,10 +1638,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
constexpr SizeT kMaxIndexedBufferBindings = 64;
|
||||
IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings];
|
||||
IndexedBufferBinding g_indexedSSBOBindings[kMaxIndexedBufferBindings];
|
||||
// Transform feedback gets a shadow for a reason the other two do not have: the
|
||||
// capture points are synced from the application's TOUCHED high-water mark, which
|
||||
// deqp/glcts permanently raises to GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by
|
||||
// clearing every point after each test case. Without a shadow every capture that
|
||||
// uses fewer points than that (i.e. every INTERLEAVED_ATTRIBS capture) re-issued a
|
||||
// redundant glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) for the unused
|
||||
// tail immediately before glBeginTransformFeedback - calls a plain GL application
|
||||
// never makes there, and the only thing MobileGL does differently from one.
|
||||
//
|
||||
// Unlike the UBO/SSBO points these are NOT context state: they belong to the bound
|
||||
// transform feedback OBJECT, so XfbImpl::BindTransformFeedback drops the whole
|
||||
// shadow to unknown on every object switch (InvalidateTransformFeedbackBindingShadows).
|
||||
IndexedBufferBinding g_indexedXFBBindings[kMaxIndexedBufferBindings];
|
||||
IndexedBufferBinding* IndexedBindingShadow(GLenum glTarget, Uint index) {
|
||||
if (index >= kMaxIndexedBufferBindings) return nullptr; // out of range: never cache
|
||||
if (glTarget == GL_UNIFORM_BUFFER) return &g_indexedUBOBindings[index];
|
||||
if (glTarget == GL_SHADER_STORAGE_BUFFER) return &g_indexedSSBOBindings[index];
|
||||
if (glTarget == GL_TRANSFORM_FEEDBACK_BUFFER) return &g_indexedXFBBindings[index];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1403,6 +1672,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (auto& binding : g_indexedSSBOBindings) {
|
||||
if (binding.id == id) binding = {};
|
||||
}
|
||||
for (auto& binding : g_indexedXFBBindings) {
|
||||
if (binding.id == id) binding = {};
|
||||
}
|
||||
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
|
||||
g_boundPixelPackBufferId = 0;
|
||||
}
|
||||
@@ -1419,9 +1691,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (auto& binding : g_indexedSSBOBindings) {
|
||||
if (binding.id == id) binding.known = false;
|
||||
}
|
||||
for (auto& binding : g_indexedXFBBindings) {
|
||||
if (binding.id == id) binding.known = false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The capture points belong to the bound transform feedback object, so a bind (or a
|
||||
// delete, which reverts to the default object) replaces all of them at once with
|
||||
// state this shadow has never seen. Distrust rather than scrub: the driver's bindings
|
||||
// are whatever the newly bound object holds, which is NOT necessarily base(0), and
|
||||
// scrubbing would let a later bind of 0 be false-skipped.
|
||||
void InvalidateTransformFeedbackBindingShadows() {
|
||||
for (auto& binding : g_indexedXFBBindings) binding.known = false;
|
||||
}
|
||||
|
||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
|
||||
auto* s = IndexedBindingShadow(glTarget, index);
|
||||
if (s && s->known && s->isBase && s->id == id) return;
|
||||
@@ -1439,6 +1723,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void InvalidateIndexedBufferBindingCache() {
|
||||
for (auto& b : g_indexedUBOBindings) b = {};
|
||||
for (auto& b : g_indexedSSBOBindings) b = {};
|
||||
for (auto& b : g_indexedXFBBindings) b = {};
|
||||
}
|
||||
|
||||
void TrimBufferPool() {
|
||||
@@ -1591,6 +1876,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"ring offset mask below requires power-of-two ring sizes");
|
||||
static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0,
|
||||
"ring offset mask below requires power-of-two ring sizes");
|
||||
static_assert((kUploadRingInitialBytes & (kUploadRingInitialBytes - 1)) == 0,
|
||||
"ring offset mask below requires power-of-two ring sizes");
|
||||
const SizeT offset = static_cast<SizeT>(store.head & (store.size - 1));
|
||||
if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) {
|
||||
store.head += alignedSize;
|
||||
@@ -1730,7 +2017,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace
|
||||
|
||||
Bool UboRingAvailable() {
|
||||
if (MG_Config::Features.DisableUboRing) return false;
|
||||
if (MG_Config::Features.EsprytDisableUboRing) return false;
|
||||
return RingAvailable(g_uboRing);
|
||||
}
|
||||
|
||||
@@ -1743,7 +2030,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void UboRingOnPresent() { RingOnPresent(g_uboRing); }
|
||||
|
||||
Bool UnpackRingAvailable() {
|
||||
if (MG_Config::Features.DisableUnpackRing) return false;
|
||||
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
|
||||
return RingAvailable(g_unpackRing);
|
||||
}
|
||||
|
||||
@@ -1759,6 +2046,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
|
||||
|
||||
void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); }
|
||||
|
||||
void UploadRingOnPresent() { RingOnPresent(g_uploadRing); }
|
||||
} // namespace BufferImpl
|
||||
|
||||
namespace VertexArrayImpl {
|
||||
@@ -2455,7 +2744,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||
// TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes.
|
||||
// Discriminator for the mali-mem-purge hiccup theory: never hand the
|
||||
// driver a texture free, so the purge daemon has nothing to reclaim.
|
||||
static const Bool s_expLeakTextureDeletes = [] {
|
||||
FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb");
|
||||
if (!f) return false;
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}();
|
||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures &&
|
||||
!s_expLeakTextureDeletes) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
if (m_bufferImageSplitViewId != 0) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
||||
@@ -3231,6 +3530,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
||||
return data;
|
||||
}
|
||||
// With the storage widened to 8-bit-per-channel (the packed16 field-order quirk)
|
||||
// there is no driver requantization left for the repack to pre-empt - the shadow's
|
||||
// UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg
|
||||
// retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all.
|
||||
if (TextureImpl::UsesWidenedPacked16NormStorage(format)) {
|
||||
return data;
|
||||
}
|
||||
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
|
||||
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
|
||||
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
|
||||
@@ -5753,6 +6059,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint g_fragColorBroadcastCount = 1;
|
||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||
Uint g_lastUsedBackendProgramId = 0;
|
||||
// Every error-queue drain in the program build path is bounded by this: a lost
|
||||
// context never answers GL_NO_ERROR, and the build runs on the thread that would
|
||||
// then spin forever.
|
||||
constexpr Int kMaxDrainedProgramErrors = 32;
|
||||
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
||||
|
||||
BackendProgramObjectImpl::BackendProgramObjectImpl() {
|
||||
@@ -6267,7 +6577,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames, const Bool stripInputBlockLocations,
|
||||
const Bool stripOutputBlockLocations,
|
||||
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
|
||||
String& outError) const {
|
||||
@@ -6304,7 +6615,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Int advertisedMaxSamples =
|
||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
|
||||
// by default (MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
|
||||
// by default (MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION). Having the extension is not a
|
||||
// reason to keep the builtin: it only ever gave the SHADER a compilable name, while
|
||||
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
|
||||
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
|
||||
@@ -6658,6 +6969,54 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &atomicCounterSpirv;
|
||||
}
|
||||
|
||||
// The second half of the inter-stage interface-block repair, and the one that
|
||||
// actually closes the 420pack group: this driver drops the payload of a block that
|
||||
// carries an explicit layout(location=) whenever a tessellation or geometry stage
|
||||
// is in the pipeline, so the qualifier comes off and ES matches the block by name
|
||||
// and member sequence instead. The names those two sides agree on are the ones the
|
||||
// rename above just fixed, which is why this runs AFTER it and not before.
|
||||
//
|
||||
// The caller arms the two directions; both are false unless the driver POST
|
||||
// measured the defect AND this program has a stage that can hit it. Adopted only
|
||||
// when this stage really had a located block, for the reason the array-input split
|
||||
// documents: the optimizer hands back a re-serialised copy either way.
|
||||
//
|
||||
// LAST IN THE CHAIN, and that position is load-bearing. Vulkan SPIR-V REQUIRES a
|
||||
// Location on every user-defined Input/Output variable
|
||||
// ([VUID-StandaloneSpirv-Location-04915]), so the module this produces is
|
||||
// deliberately no longer valid Vulkan SPIR-V - it is an ESSL-emission intermediate
|
||||
// that goes straight into SPIRV-Cross and reaches no driver as SPIR-V. Running it
|
||||
// here means no later pass validates what it produced; the pass itself skips
|
||||
// validation for the same reason (see StripIoBlockLocationsForEssl). Anywhere
|
||||
// earlier and every remaining pass would latch a validation failure on a module
|
||||
// that is doing exactly what it was asked to.
|
||||
Vector<unsigned int> strippedIoBlockLocationSpirv;
|
||||
if (stripInputBlockLocations || stripOutputBlockLocations) {
|
||||
Bool strippedAny = false;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripIoBlockLocationsForEssl(
|
||||
*effectiveSpirv, stripInputBlockLocations, stripOutputBlockLocations,
|
||||
strippedAny, strippedIoBlockLocationSpirv, enableSpirvValidation) &&
|
||||
!strippedIoBlockLocationSpirv.empty() && strippedAny) {
|
||||
effectiveSpirv = &strippedIoBlockLocationSpirv;
|
||||
// THE ARMING SIGNAL, and it is INFO on purpose: the per-stage line below is
|
||||
// MGLOG_D, which is compiled out of every build CI and the device runs, so
|
||||
// nothing outside a debug build could tell an armed repair from a silently
|
||||
// un-armed one. Latched, so it costs one line per process rather than one
|
||||
// per stage of every program. The integration lane that pins the emulation
|
||||
// on asserts on exactly this line - see UnlocatedIoBlockScenario.
|
||||
MGLOG_I_ONCE("DirectGLES is emitting inter-stage interface blocks WITHOUT their "
|
||||
"layout(location) qualifier, because this driver loses a located "
|
||||
"block's payload across a tessellation or geometry boundary.");
|
||||
MGLOG_D("Program %u stage %s: interface-block location qualifiers dropped "
|
||||
"(%s), because this driver loses a located block's payload across a "
|
||||
"tessellation or geometry boundary.",
|
||||
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
stripInputBlockLocations
|
||||
? (stripOutputBlockLocations ? "consumed and produced" : "consumed")
|
||||
: "produced");
|
||||
}
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
@@ -6812,10 +7171,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const String outMembers =
|
||||
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
|
||||
|
||||
const String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
|
||||
inMembers, outMembers,
|
||||
m_passthroughTessControlOuterLevel,
|
||||
m_passthroughTessControlInnerLevel);
|
||||
String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
|
||||
inMembers, outMembers,
|
||||
m_passthroughTessControlOuterLevel,
|
||||
m_passthroughTessControlInnerLevel);
|
||||
// The mirrored member lists can carry gl_PointSize - the neighbour stage declared it,
|
||||
// so matching it is the whole point - and a redeclaration is exactly as illegal as a
|
||||
// reference in ESSL without the extension. Same directive, same never-speculative
|
||||
// rule as the per-stage loop; a driver with neither spelling gets nothing added and
|
||||
// fails below with its own message, which is the honest outcome for a shape it
|
||||
// cannot express.
|
||||
const char* passthroughPointSizeExtension =
|
||||
source.find("gl_PointSize") != String::npos
|
||||
? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true)
|
||||
: nullptr;
|
||||
source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension);
|
||||
|
||||
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
|
||||
if (backendShaderId == 0) {
|
||||
@@ -6975,6 +7345,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
if (stateProgramObject->PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that
|
||||
// pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because
|
||||
// every rendering assertion stays green on a healthy driver whether the
|
||||
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||
MGLOG_I_ONCE("DirectGLES is building programs whose tessellation/geometry gl_PointSize was "
|
||||
"demoted to an ordinary varying, because this driver cannot host the built-in "
|
||||
"in those stages.");
|
||||
}
|
||||
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
|
||||
for (const auto& ref : stateProgramObject->GetLinkedShaderSnapshot()) {
|
||||
if (!ref.shader) continue;
|
||||
@@ -7042,6 +7421,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
|
||||
if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true;
|
||||
}
|
||||
// A SECOND, INDEPENDENT interface-block repair riding the same gate, because it
|
||||
// needs the same question answered: "does this program have a stage where an
|
||||
// inter-stage block can go wrong?". CanDeclareBlocksInBothDirections is true for
|
||||
// exactly the tessellation and geometry stages, which is also exactly the set of
|
||||
// stages whose presence makes this driver drop a LOCATED block's payload (a
|
||||
// vertex-to-fragment located block is fine on the same driver, measured). The two
|
||||
// repairs are otherwise unrelated: the rename fixes a name collision inside ONE
|
||||
// stage, this drops a qualifier from EVERY block of the program - so it does not
|
||||
// wait for the collision probe to find anything.
|
||||
const Bool ioBlockLocationStripArmed =
|
||||
!g_GLESCapabilities.SupportsLocatedInterStageIoBlocks &&
|
||||
anyStageCanDeclareBlocksInBothDirections;
|
||||
if (anyStageCanDeclareBlocksInBothDirections) {
|
||||
for (SizeT index = 0; index < shaderSpirvs.size(); ++index) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
|
||||
@@ -7236,6 +7627,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
|
||||
esslKeyInputs.outputBlockRenames = &outputBlockRenames;
|
||||
|
||||
// ...and THIS STAGE's share of the interface-block LOCATION strip, planned the
|
||||
// same way and for the same reason. The gate has three parts, all of which have
|
||||
// to hold before a single block loses its qualifier:
|
||||
// * the driver POST measured the defect (never a renderer-string quirk list);
|
||||
// * this program has a stage that can hit it - a located block between a
|
||||
// vertex and a fragment stage works on the affected driver, so a program
|
||||
// with neither tessellation nor geometry keeps its ESSL byte for byte;
|
||||
// * for THIS stage and THIS direction, this program HAS a stage on that side
|
||||
// of it. That is the same test the rename plan above makes, and the same
|
||||
// approximation: it asks "is some stage of this program earlier/later than
|
||||
// me", not "is the exact partner of every one of my blocks here". The two
|
||||
// coincide for every program MobileGL builds, because a separable pipeline
|
||||
// is flattened into one composite carrying every stage that has a shader
|
||||
// (GLContext::GetProgramForDraw) and a program bound with glUseProgram has
|
||||
// no partner program at all - so a stage set with a gap in it does not
|
||||
// arise. Should one ever arise, this must become the nearest-stage
|
||||
// resolution the rename plan computes, or the two ends of the gap would
|
||||
// disagree about the qualifier.
|
||||
// The direction tests deliberately mirror that plan rather than inventing a
|
||||
// second rule for the same question.
|
||||
Bool stripInputBlockLocations = false;
|
||||
Bool stripOutputBlockLocations = false;
|
||||
if (ioBlockLocationStripArmed && stagePipelineIndices[index] >= 0) {
|
||||
const Int myPipelineIndex = stagePipelineIndices[index];
|
||||
for (const Int otherPipelineIndex : stagePipelineIndices) {
|
||||
if (otherPipelineIndex < 0) continue;
|
||||
if (otherPipelineIndex < myPipelineIndex) stripInputBlockLocations = true;
|
||||
if (otherPipelineIndex > myPipelineIndex) stripOutputBlockLocations = true;
|
||||
}
|
||||
}
|
||||
esslKeyInputs.stripInputBlockLocations = stripInputBlockLocations;
|
||||
esslKeyInputs.stripOutputBlockLocations = stripOutputBlockLocations;
|
||||
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
|
||||
|
||||
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
|
||||
@@ -7262,6 +7686,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
|
||||
imageFormatBake, storageBlockBindingOverrides,
|
||||
inputBlockRenames, outputBlockRenames,
|
||||
stripInputBlockLocations, stripOutputBlockLocations,
|
||||
m_atomicCounterEsslBindingTop,
|
||||
enableSpirvValidation, source,
|
||||
stageFlattenedXfbBlockNames,
|
||||
@@ -7339,6 +7764,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
source.find("gl_ViewportIndex") != String::npos;
|
||||
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
|
||||
|
||||
// The fourth header-level rewrite, and the same shape as the third: ESSL has no
|
||||
// gl_PointSize in a tessellation or geometry stage at ANY version - 320 makes the
|
||||
// stages core and still leaves the built-in behind EXT/OES_..._point_size - while
|
||||
// SPIRV-Cross prints it bare. Without the directive the stage fails to compile
|
||||
// with "`gl_PointSize' undeclared", which takes the whole program to program 0:
|
||||
// the draw renders nothing AND glBeginTransformFeedback is rejected, so a capture
|
||||
// of anything at all off that program silently comes back empty. The token probe
|
||||
// keeps the line off every other program and PointSizeExtensionName returns
|
||||
// nullptr - i.e. nothing is emitted - on a driver advertising neither spelling.
|
||||
if (source.find("gl_PointSize") != String::npos) {
|
||||
const Bool tessellationStage = glShaderType == GL_TESS_CONTROL_SHADER ||
|
||||
glShaderType == GL_TESS_EVALUATION_SHADER;
|
||||
if (tessellationStage || glShaderType == GL_GEOMETRY_SHADER) {
|
||||
const auto tier = tessellationStage ? g_GLESCapabilities.TessellationPointSizeSupport
|
||||
: g_GLESCapabilities.GeometryPointSizeSupport;
|
||||
const char* pointSizeExtension = PointSizeExtensionName(tier, tessellationStage);
|
||||
if (pointSizeExtension == nullptr) {
|
||||
// Latched, and an ERROR rather than a warning: what follows is a
|
||||
// driver compile failure whose text names a built-in the application
|
||||
// never mis-spelled, and the reason is a missing driver capability
|
||||
// rather than anything in the shader. Saying so here is the whole
|
||||
// difference between a legible skip and an unexplained black draw.
|
||||
MGLOG_E_ONCE("This driver advertises neither the EXT nor the OES %s_point_size "
|
||||
"extension, so its ESSL has no gl_PointSize in a %s stage; program %u "
|
||||
"will fail to compile. Point size from a non-vertex stage is not "
|
||||
"available on this device.",
|
||||
tessellationStage ? "tessellation" : "geometry",
|
||||
tessellationStage ? "tessellation" : "geometry",
|
||||
stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
source = RequestPointSizeExtension(std::move(source), pointSizeExtension);
|
||||
}
|
||||
}
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
// The completion half of the format bake, for the formats SPIRV-Cross throws on
|
||||
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
|
||||
@@ -7460,12 +7919,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// carried 294 INFO lines and zero ERROR lines while two generated shaders
|
||||
// were being rejected outright, and the lane could not say why it was
|
||||
// rendering an empty translucent layer. A shader the driver refuses is
|
||||
// never noise, and one line per refused shader is bounded by program count.
|
||||
// never noise.
|
||||
//
|
||||
// A BOUNDED EXCERPT of the source goes with it. The driver log names a line
|
||||
// and a column in text that exists nowhere but here, so without any source at
|
||||
// all the only way to read "`gl_PointSize' undeclared" is to rebuild the whole
|
||||
// library at DEBUG - but the full dump cannot go at E either. This is not
|
||||
// "one line per refused shader": SyncToBackend's rebuild gate keys on
|
||||
// per-draw state (the enabled-draw-buffer count among it), so a program used
|
||||
// across passes with different draw-buffer counts re-transpiles, re-compiles
|
||||
// and re-fails on every alternation, i.e. per frame. At E - live at the
|
||||
// production INFO level - each of those records would push the whole
|
||||
// post-SPIRV-Cross ESSL through the global log mutex with a forced flush onto
|
||||
// /sdcard/MG/latest.log, the file users are asked to share. The excerpt keeps
|
||||
// the record O(1); the full text is still there at D, printed against this
|
||||
// same backend shader id by the "Setting shader source" line above, so
|
||||
// nothing needs to be dumped twice.
|
||||
constexpr SizeT kMaxLoggedSourceBytes = 2048;
|
||||
String truncatedSource;
|
||||
const char* sourceForLog = source.c_str();
|
||||
if (source.size() > kMaxLoggedSourceBytes) {
|
||||
// Back up to a line boundary when there is one inside the window, so the
|
||||
// excerpt ends on a whole statement rather than mid-token. Built only on
|
||||
// this branch: a stage that fits keeps its own buffer and is not copied.
|
||||
SizeT cut = kMaxLoggedSourceBytes;
|
||||
if (const SizeT lastNewline = source.rfind('\n', cut);
|
||||
lastNewline != String::npos && lastNewline > 0) {
|
||||
cut = lastNewline + 1;
|
||||
}
|
||||
truncatedSource = source.substr(0, cut);
|
||||
truncatedSource += "... [" + std::to_string(source.size() - cut) +
|
||||
" more bytes; the whole stage is printed at the DEBUG level]\n";
|
||||
sourceForLog = truncatedSource.c_str();
|
||||
}
|
||||
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
|
||||
"%u, driver log: %s",
|
||||
"%u, driver log: %s\nSource:\n%s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
|
||||
log.data());
|
||||
log.data(), sourceForLog);
|
||||
m_backendProgramUsable = false;
|
||||
// Nothing will ever attach this one, so nothing else can free it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
@@ -7523,6 +8014,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// program before it links. SPIRV-Cross keeps user output names verbatim in
|
||||
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
|
||||
// frontend's requested names carry over unchanged.
|
||||
SizeT declaredXfbVaryingCount = 0;
|
||||
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
|
||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
|
||||
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
|
||||
@@ -7532,6 +8024,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// for; it has the variable that replaced it. Everything else - including a
|
||||
// member of a block that was left alone - keeps the application's spelling.
|
||||
// Storage first, pointers after: xfbNames holds pointers into these strings.
|
||||
//
|
||||
// Same rule for a demoted gl_PointSize: the capture stage's ESSL no longer
|
||||
// spells the built-in at all - the value lives in the carrier the demotion
|
||||
// named - so the driver-side request has to follow it there. Only when the
|
||||
// capture stage IS a demoted one (geometry, else evaluation): a program whose
|
||||
// capture stage is the vertex shader keeps the built-in and its spelling,
|
||||
// whatever happened to a control stage behind it.
|
||||
Bool captureStageDemoted = false;
|
||||
if (stateProgramObject->PointSizeDemoted()) {
|
||||
for (const ShaderStage linkedStage : linkedStages) {
|
||||
if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) {
|
||||
captureStageDemoted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector<String> rewrittenXfbNames(xfbVaryings.size());
|
||||
for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) {
|
||||
String flatName;
|
||||
@@ -7539,6 +8047,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(
|
||||
xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) {
|
||||
rewrittenXfbNames[nameIndex] = std::move(flatName);
|
||||
} else if (captureStageDemoted && xfbVaryings[nameIndex].name == "gl_PointSize") {
|
||||
rewrittenXfbNames[nameIndex] =
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME;
|
||||
} else {
|
||||
rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name;
|
||||
}
|
||||
@@ -7548,9 +8059,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
|
||||
m_backendProgramId);
|
||||
// Bounded: a lost context never answers GL_NO_ERROR, and this runs on the
|
||||
// thread that would then spin forever.
|
||||
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
|
||||
xfbNames.data(),
|
||||
stateProgramObject->GetTransformFeedbackBufferMode());
|
||||
// Unchecked before. A rejected capture set leaves the program linking happily
|
||||
// with NO capture set at all, and then every draw of every span records
|
||||
// nothing while the application reads its buffer's pre-draw bytes and
|
||||
// GL_NO_ERROR - the signature four conformance families were stuck on.
|
||||
if (const GLenum xfbError = g_GLESFuncs.glGetError(); xfbError != GL_NO_ERROR) {
|
||||
String declared;
|
||||
for (const auto& xfbName : rewrittenXfbNames) {
|
||||
if (!declared.empty()) declared += ", ";
|
||||
declared += xfbName;
|
||||
}
|
||||
MGLOG_E("The ES driver REJECTED the transform feedback capture set for backend program %u with "
|
||||
"%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.",
|
||||
m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(
|
||||
stateProgramObject->GetTransformFeedbackBufferMode()).c_str(),
|
||||
declared.c_str(), stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
declaredXfbVaryingCount = xfbNames.size();
|
||||
}
|
||||
|
||||
// Link program
|
||||
@@ -7593,6 +8126,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} else {
|
||||
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
|
||||
// A link that SUCCEEDS can still have dropped the capture set: ESSL rejects a
|
||||
// requested name the transpiled shader does not actually declare by simply not
|
||||
// capturing it, and a program whose last vertex-processing stage was rewritten
|
||||
// by a SPIR-V pass (viewport-index lowering, gl_PerVertex handling, the
|
||||
// synthesized pass-through tessellation control stage) can end up spelling its
|
||||
// outputs differently from the frontend's request. Asking the driver what it
|
||||
// ACTUALLY linked is the only way to tell that apart from a driver that just
|
||||
// captures nothing - which is the whole ambiguity the empty-capture failures
|
||||
// across geometry_shader / tessellation_shader / gpu_shader5 / DSA sat on.
|
||||
if (declaredXfbVaryingCount > 0) {
|
||||
GLint linkedXfbVaryings = 0;
|
||||
GLint linkedXfbBufferMode = 0;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_VARYINGS,
|
||||
&linkedXfbVaryings);
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_TRANSFORM_FEEDBACK_BUFFER_MODE,
|
||||
&linkedXfbBufferMode);
|
||||
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
const GLenum requestedMode = stateProgramObject->GetTransformFeedbackBufferMode();
|
||||
if (static_cast<SizeT>(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount ||
|
||||
static_cast<GLenum>(linkedXfbBufferMode) != requestedMode) {
|
||||
MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not "
|
||||
"agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) "
|
||||
"in mode %s. Captures made with it will be empty or wrongly laid out.",
|
||||
m_backendProgramId, stateProgramObject->GetExternalIndex(), declaredXfbVaryingCount,
|
||||
MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings,
|
||||
MG_Util::ConvertGLEnumToString(
|
||||
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
|
||||
} else {
|
||||
MGLOG_D("Backend program %u capture set confirmed by the driver: %d varying(s), mode %s",
|
||||
m_backendProgramId, linkedXfbVaryings,
|
||||
MG_Util::ConvertGLEnumToString(
|
||||
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
// The driver program was relinked IN PLACE, so its GL name no longer identifies
|
||||
// the executable behind it - and that name is exactly what Use()'s
|
||||
|
||||
@@ -230,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
|
||||
@@ -461,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
|
||||
@@ -544,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 /
|
||||
@@ -618,7 +637,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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_DISABLE_UNPACK_RING),
|
||||
// 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.
|
||||
@@ -633,6 +652,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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 {
|
||||
@@ -1624,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,
|
||||
|
||||
@@ -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>
|
||||
@@ -125,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
|
||||
@@ -182,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
|
||||
@@ -713,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
|
||||
|
||||
@@ -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
|
||||
@@ -507,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
|
||||
|
||||
|
||||
@@ -624,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
|
||||
@@ -1081,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,7 +70,7 @@ 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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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(
|
||||
@@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
|
||||
@@ -44,6 +44,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (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;
|
||||
|
||||
@@ -77,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
// Where a gl_PerVertex built-in output lives, resolved from the module's annotations.
|
||||
// Named for gl_Position because the clip-space fixup is what it was written for, and it
|
||||
// is still the only shape that pass accepts - but the transform-feedback capture pass
|
||||
// resolves gl_PointSize through the same struct, in which case `vectorTypeId` /
|
||||
// `vectorPtrTypeId` hold the SCALAR float type and its Output pointer rather than a vec4.
|
||||
struct PositionTargetInfo {
|
||||
Uint32 variableId = 0;
|
||||
Uint32 vectorTypeId = 0;
|
||||
@@ -102,6 +107,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_PointSize's counterpart to IsVec4Float32. The two are the only shapes any
|
||||
// gl_PerVertex member this file resolves can have, and each resolver takes whichever
|
||||
// one its built-in is declared with, so a mismatched type declines rather than
|
||||
// producing a mirror the driver would reject.
|
||||
Bool IsFloat32Scalar(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||
auto* floatInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
|
||||
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
|
||||
|
||||
if (outFloatTypeId) *outFloatTypeId = typeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Which of the two shapes above a resolver should accept. A plain function pointer
|
||||
// rather than a std::function: every call site is one of the two free functions.
|
||||
using BuiltInTypeCheckFn = Bool (*)(spvtools::opt::IRContext*, Uint32, Uint32*);
|
||||
|
||||
spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) {
|
||||
if (variable.type_description == nullptr) {
|
||||
return SPVC_BASETYPE_UNKNOWN;
|
||||
@@ -381,9 +403,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return used;
|
||||
}
|
||||
|
||||
void ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) {
|
||||
// What a failed validation says, for a caller that wants to put it in its own message.
|
||||
struct SpirvValidationFailure {
|
||||
String message;
|
||||
Int result = 0;
|
||||
SizeT index = 0;
|
||||
};
|
||||
|
||||
// Returns whether the module validates. The result used to be discarded everywhere: the
|
||||
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
|
||||
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
|
||||
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
|
||||
// the same "not a validating entry point" behaviour PipelineFactory already documents for
|
||||
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
|
||||
//
|
||||
// This function does NOT log the failure at E any more. It used to, unlatched, on the
|
||||
// stated grounds that "reaching here already requires the validation switch to be armed,
|
||||
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
|
||||
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
|
||||
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
|
||||
// The failure text now travels back through `outFailure` so the LATCHED call-site
|
||||
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
|
||||
// is the D-level detail and the process-wide counter the test lanes assert on.
|
||||
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
|
||||
SpirvValidationFailure* outFailure = nullptr) {
|
||||
if (outFailure != nullptr) *outFailure = {};
|
||||
if (spirv.empty()) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
spv_const_binary_t binary = {spirv.data(), spirv.size()};
|
||||
@@ -405,18 +451,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
const char* message =
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
|
||||
const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
|
||||
// The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
|
||||
// asserting on the delta). Bumped for every failed validation, including one a
|
||||
// caller goes on to recover from: a transform that produced an invalid module is
|
||||
// a real defect whether or not this run survived it.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_E(
|
||||
if (outFailure != nullptr) {
|
||||
*outFailure = {String(message), static_cast<Int>(result), index};
|
||||
}
|
||||
MGLOG_D(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
index,
|
||||
message);
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
@@ -432,6 +484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
spvValidatorOptionsDestroy(options);
|
||||
spvContextDestroy(context);
|
||||
return result == SPV_SUCCESS;
|
||||
}
|
||||
|
||||
void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable,
|
||||
@@ -743,8 +796,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveDirectBuiltInTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
|
||||
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
|
||||
if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false;
|
||||
@@ -756,7 +809,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
target.variableId = variableId;
|
||||
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
|
||||
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
if (!typeCheck(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
target.vectorPtrTypeId = varInst->type_id();
|
||||
target.isMember = false;
|
||||
|
||||
@@ -771,15 +824,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return context->get_type_mgr()->GetTypeInstruction(&ptrType);
|
||||
}
|
||||
|
||||
Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveMemberBuiltInTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
|
||||
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
if (memberIndex >= structInst->NumInOperands()) return false;
|
||||
|
||||
const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
|
||||
Uint32 floatTypeId = 0;
|
||||
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false;
|
||||
if (!typeCheck(context, vectorTypeId, &floatTypeId)) return false;
|
||||
|
||||
const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
|
||||
if (vectorPtrTypeId == 0) return false;
|
||||
@@ -807,27 +860,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
// The OUTPUT variable (or gl_PerVertex member) carrying `builtIn`, if the module
|
||||
// declares one of the expected type. Annotations are the search space deliberately:
|
||||
// they survive the link-time sanitize chain's interface delisting, which is the whole
|
||||
// reason EnsureEntryPointInterface exists.
|
||||
Bool FindBuiltInTarget(spvtools::opt::IRContext* context, spv::BuiltIn builtIn,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
Vector<Pair<Uint32, Uint32>> memberCandidates;
|
||||
constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn);
|
||||
constexpr auto kBuiltInPosition = static_cast<Uint32>(spv::BuiltIn::Position);
|
||||
const auto wantedBuiltIn = static_cast<Uint32>(builtIn);
|
||||
|
||||
for (auto& inst : context->module()->annotations()) {
|
||||
if (inst.opcode() == spv::Op::OpDecorate) {
|
||||
if (inst.NumInOperands() < 3) continue;
|
||||
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue;
|
||||
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true;
|
||||
if (inst.GetSingleWordInOperand(2) != wantedBuiltIn) continue;
|
||||
if (ResolveDirectBuiltInTarget(context, inst.GetSingleWordInOperand(0), typeCheck, outTarget)) {
|
||||
return true;
|
||||
}
|
||||
} else if (inst.opcode() == spv::Op::OpMemberDecorate) {
|
||||
if (inst.NumInOperands() < 4) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != wantedBuiltIn) continue;
|
||||
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [structTypeId, memberIndex] : memberCandidates) {
|
||||
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true;
|
||||
if (ResolveMemberBuiltInTarget(context, structTypeId, memberIndex, typeCheck, outTarget)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
return FindBuiltInTarget(context, spv::BuiltIn::Position, IsVec4Float32, outTarget);
|
||||
}
|
||||
|
||||
// Put `variableId` back on `entryPoint`'s interface list if it is not already there.
|
||||
//
|
||||
// SPIR-V requires every Input/Output global an entry point statically uses to be listed on
|
||||
// its OpEntryPoint, and spirv-val enforces it ("Interface variable id <N> is used by entry
|
||||
// point 'main' id <M>, but is not listed as an interface"). The link-time sanitize chain
|
||||
// DELISTS a variable nothing referenced yet - ShaderCompiler::SanitizeAndOptimizeBinary
|
||||
// runs CreateAggressiveDCEPass(false), which may never delete an Output, followed by
|
||||
// CreateRemoveUnusedInterfaceVariablesPass, which rebuilds the operand list from the
|
||||
// variables actually referenced. A TES that redeclares `out gl_PerVertex { vec4
|
||||
// gl_Position; }` and never writes it therefore reaches the backend with the OpVariable
|
||||
// and its BuiltIn Position decoration intact and its interface slot gone. Any pass that
|
||||
// then injects a reference has to put the slot back, or it hands the driver a module no
|
||||
// validator accepts - and Mali r54 answers that with a SIGSEGV inside pipeline creation
|
||||
// rather than an error return.
|
||||
//
|
||||
// No SPIR-V version gate here, unlike GlFragCoordYFlipPass's identical call for its
|
||||
// injected PRIVATE global: Input and Output belong on the interface in every version,
|
||||
// and only 1.4 widened it to the other storage classes.
|
||||
Bool EnsureEntryPointInterface(spvtools::opt::IRContext* context, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 variableId) {
|
||||
// In-operands: 0 = execution model, 1 = entry function id, 2 = name, 3.. = interface.
|
||||
constexpr Uint32 kFirstInterfaceOperand = 3;
|
||||
if (variableId == 0) return false;
|
||||
for (Uint32 operand = kFirstInterfaceOperand; operand < entryPoint.NumInOperands(); ++operand) {
|
||||
if (entryPoint.GetSingleWordInOperand(operand) == variableId) return false;
|
||||
}
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
|
||||
context->AnalyzeUses(&entryPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is `pointerId` the position target itself, or an access chain rooted at it?
|
||||
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
|
||||
const PositionTargetInfo& target) {
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
for (Uint32 current = pointerId; current != 0;) {
|
||||
if (current == target.variableId) return true;
|
||||
const auto* inst = defUse->GetDef(current);
|
||||
if (inst == nullptr) return false;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpPtrAccessChain:
|
||||
case spv::Op::OpInBoundsPtrAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
current = inst->GetSingleWordInOperand(0);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does anything in the module write the position target?
|
||||
//
|
||||
// Deliberately conservative - it answers "assume yes" for every shape it cannot read
|
||||
// exactly, because a false "no" would silently drop the clip-space fixup from a shader
|
||||
// that does write gl_Position, while a false "yes" only reinstates the behaviour this
|
||||
// pass has always had. Scans every function rather than just the entry point's: a shader
|
||||
// that assigns gl_Position inside a helper is still a shader that writes it, and passing
|
||||
// the pointer to a call is a write as far as this can tell.
|
||||
Bool ModuleWritesPositionTarget(spvtools::opt::IRContext* context, const PositionTargetInfo& target) {
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (const auto& inst : block) {
|
||||
switch (inst.opcode()) {
|
||||
case spv::Op::OpStore:
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(0), target)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpFunctionCall:
|
||||
// In-operand 0 is the callee; the rest are arguments.
|
||||
for (Uint32 argument = 1; argument < inst.NumInOperands(); ++argument) {
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(argument),
|
||||
target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -914,6 +1070,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
|
||||
|
||||
// Nothing to remap in a Position the shader never writes. Declining is not just
|
||||
// an optimisation: the fixup is load-modify-store, so on an unwritten Position it
|
||||
// converts "undefined, never written" into "written with whatever the load
|
||||
// returned", and the store is a reference to a variable the link-time sanitize
|
||||
// chain has already delisted from the entry-point interface. glslang emits the
|
||||
// OpVariable for every DECLARED interface block, so a redeclared-but-unwritten
|
||||
// `out gl_PerVertex` is a shape real shaders have.
|
||||
if (!ModuleWritesPositionTarget(context(), target)) {
|
||||
MGLOG_D("gl-to-vulkan-position-fix: the shader never writes gl_Position; leaving it alone");
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
|
||||
if (!floatType) return Status::SuccessWithoutChange;
|
||||
|
||||
@@ -945,6 +1113,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
|
||||
if (!function) continue;
|
||||
|
||||
Bool modifiedThisEntryPoint = false;
|
||||
for (auto& bb : *function) {
|
||||
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
|
||||
auto* inst = &*instIter;
|
||||
@@ -953,10 +1122,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
|
||||
if (!needsFixup) continue;
|
||||
|
||||
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
modifiedThisEntryPoint |=
|
||||
InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
}
|
||||
}
|
||||
// Per entry point, and only for one this pass actually injected into: the
|
||||
// injected load/store is a static use of the position variable, so the
|
||||
// variable has to be on THIS entry point's interface list.
|
||||
if (modifiedThisEntryPoint) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
modified |= modifiedThisEntryPoint;
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1235,6 +1412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool needsPositionMirror = false;
|
||||
Uint32 positionBufferIndex = 0;
|
||||
Uint32 positionOffset = 0;
|
||||
// gl_PointSize is a gl_PerVertex MEMBER, never a variable of its own, so the
|
||||
// debug-name lookup below can never resolve it - it used to fall through to
|
||||
// "no SPIR-V variable named 'gl_PointSize'" and leave the frontend's reserved
|
||||
// slot unwritten, or, when it was the only capture, leave the module with no
|
||||
// Xfb execution mode at all and the whole span declined.
|
||||
Bool needsPointSizeMirror = false;
|
||||
Uint32 pointSizeBufferIndex = 0;
|
||||
Uint32 pointSizeOffset = 0;
|
||||
for (const auto& varying : m_varyings) {
|
||||
if (varying.name == "gl_Position") {
|
||||
needsPositionMirror = true;
|
||||
@@ -1242,6 +1427,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
positionOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.name == "gl_PointSize") {
|
||||
// A demoted module (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
|
||||
// built-in member - the value lives in the carrier variable the demotion
|
||||
// named - so the capture binds to the carrier directly. The mirror below
|
||||
// must not run for it: reading the now-unwritten member would capture
|
||||
// garbage, and the read itself is the capability access the demotion
|
||||
// exists to remove. Detected off the module's own debug names, so a
|
||||
// composite built from another program's stage answers for the module it
|
||||
// actually contains.
|
||||
const auto carrierIt = idsByName.find(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
|
||||
if (carrierIt != idsByName.end()) {
|
||||
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
continue;
|
||||
}
|
||||
needsPointSizeMirror = true;
|
||||
pointSizeBufferIndex = varying.bufferIndex;
|
||||
pointSizeOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.blockMemberIndex >= 0) {
|
||||
// glslang names the block's instance variable and its struct type
|
||||
// separately; an anonymous instance leaves only the type named, so
|
||||
@@ -1306,8 +1513,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::Position, IsVec4Float32,
|
||||
"gl_Position", positionBufferIndex, positionOffset,
|
||||
decorateForXfb);
|
||||
}
|
||||
if (needsPointSizeMirror) {
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::PointSize, IsFloat32Scalar,
|
||||
"gl_PointSize", pointSizeBufferIndex,
|
||||
pointSizeOffset, decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1347,19 +1562,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// gl_Position and gl_PointSize are captured the same way and differ only in which
|
||||
// built-in is looked up and what type it has, so one injector serves both. Anything
|
||||
// else in gl_PerVertex would need its own type check before it could be added here.
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||
Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
|
||||
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
|
||||
const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
|
||||
if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: %s capture requested but no such output found", glslName);
|
||||
return false;
|
||||
}
|
||||
if (!target.isMember) {
|
||||
// Standalone gl_Position variable: decorate it directly.
|
||||
// Standalone built-in variable: decorate it directly. It still has to be
|
||||
// on the interface - a transform-feedback decoration on a variable the entry
|
||||
// point does not list captures nothing, and the sanitize chain delists an
|
||||
// unwritten one (see EnsureEntryPointInterface).
|
||||
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1413,6 +1637,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
injected = true;
|
||||
}
|
||||
}
|
||||
// The mirror was listed on the entry point above, but the loop just added a READ
|
||||
// of the SOURCE block through an access chain, and the interface rule covers
|
||||
// reads exactly as it covers writes. A built-in capture on a shader whose
|
||||
// block the sanitize chain delisted - a TES that redeclares `out gl_PerVertex`
|
||||
// and never writes it, which is what the tessellation_control_to_tessellation_
|
||||
// evaluation.gl_MaxPatchVertices_Position_PointSize bodies do - produced an
|
||||
// invalid module here for the same reason the position fixup did.
|
||||
if (injected) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
|
||||
@@ -3236,11 +3470,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
if (program.PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
|
||||
// rendering assertion above it stays green on a healthy driver whether the
|
||||
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
|
||||
"demoted to an ordinary varying, because this device cannot host the built-in "
|
||||
"in those stages.");
|
||||
}
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
if (enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
||||
// not only when the switch is armed, so the validator's static tables have to be pinned
|
||||
// against process exit in every build too.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
||||
|
||||
@@ -3264,6 +3508,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
|
||||
// These two passes INJECT references - a store for the clip fixup, an access
|
||||
// chain and a load for the gl_Position capture mirror - and a reference to a
|
||||
// variable the link-time sanitize chain delisted from the entry-point interface
|
||||
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
|
||||
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
|
||||
// is the backstop.
|
||||
//
|
||||
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
|
||||
// are not equally optional. Rewinding straight to `spv` would also throw away the
|
||||
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
|
||||
// Xfb execution mode - while the renderer decides to call
|
||||
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
|
||||
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
|
||||
// into a transform-feedback span, violating
|
||||
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
|
||||
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
|
||||
// capture working and costs only the clip-space remap.
|
||||
//
|
||||
// Once per program on a cache miss, and only for the single stage that carries the
|
||||
// fixups - not per draw and not per module.
|
||||
SpirvValidationFailure fixupFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
|
||||
&fixupFailure)) {
|
||||
SpirvValidationFailure xfbFailure{};
|
||||
if (fixupInput != &spv &&
|
||||
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the capture-decorated one, so this program draws "
|
||||
"without the clip-space remap",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = *fixupInput;
|
||||
} else {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the untransformed one",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
@@ -3472,15 +3756,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only
|
||||
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
|
||||
// by here the descriptor bindings have been remapped and the layout about to be
|
||||
// reflected describes the remapped module, so there is no module left that is both
|
||||
// valid and consistent with it to fall back to. The recovery lives one step earlier,
|
||||
// at the clip/XFB fixups (see the revert there) - which is where a transform can
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
// varying and changed nothing, anything later that might strip it. The renderer picks
|
||||
// its capture commands from GL state alone and would otherwise open a span against a
|
||||
// pipeline that cannot feed it.
|
||||
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0 &&
|
||||
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
|
||||
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
|
||||
"than recorded against a pipeline that cannot feed them",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.xfbCaptureDeclined = true;
|
||||
}
|
||||
|
||||
// Does this stage need a device feature the device did not give us? Asked ONLY when
|
||||
// the feature is off, so a device that has it - the common case - pays nothing: the
|
||||
// whole test is short-circuited before the module is parsed.
|
||||
//
|
||||
// gl_PointSize is an ordinary per-vertex output in desktop GL and any
|
||||
// vertex-processing stage may write it, but Vulkan puts the built-in behind
|
||||
// shaderTessellationAndGeometryPointSize in the tessellation and geometry stages
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). glslang emits TessellationPointSize /
|
||||
// GeometryPointSize from the application's own access, so this program is legal GL
|
||||
// that this device cannot run - the same shape the DirectGLES arm reports when a
|
||||
// driver advertises neither EXT nor OES point-size extension, and it deserves the
|
||||
// same named message rather than a pipeline the driver may fault on.
|
||||
if (!m_tessellationAndGeometryPointSizeEnabled &&
|
||||
(stages[i] == ShaderStage::TessControl || stages[i] == ShaderStage::TessEval ||
|
||||
stages[i] == ShaderStage::Geometry) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(
|
||||
moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u stage %d accesses gl_PointSize, but this device does not "
|
||||
"support shaderTessellationAndGeometryPointSize; its draws are refused rather than "
|
||||
"built into a pipeline the driver may fault on. Point size from a non-vertex stage "
|
||||
"is not available on this device.",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.pointSizeCapabilityUnsupported = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = moduleSpv.size() * sizeof(Uint);
|
||||
@@ -3758,11 +4090,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
|
||||
// links the program at both 430 and 460.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
// Only gl_Position is written, and gl_PointSize is declared without being forwarded. That
|
||||
// is a KNOWN GAP, not a design: GL 4.6 core 11.2.2 says the fixed-function pass-through
|
||||
// hands the input patch to the evaluation stage unmodified, so an evaluation stage
|
||||
// reading gl_in[].gl_PointSize should see the vertex stage's value and instead sees
|
||||
// whatever this stage left in gl_out[] - which is nothing. A capture of it (the mirror in
|
||||
// XfbCaptureDecoratePass) faithfully records that nothing.
|
||||
//
|
||||
// The reason this comment used to give - "the renderer does not enable
|
||||
// shaderTessellationAndGeometryPointSize" - stopped being true when
|
||||
// VulkanRenderer::CreateLogicalDeviceAndQueues started taking the feature wherever the
|
||||
// device advertises it. Closing the gap is therefore possible now, but it is not free:
|
||||
// the forwarding store has to be gated on that feature, because on a device without it
|
||||
// the store is exactly the invalid usage the build-time refusal
|
||||
// (VkProgramObject::pointSizeCapabilityUnsupported) exists to keep away from the driver -
|
||||
// and this synthesized stage is not the application's, so refusing the program because
|
||||
// MobileGL's own pass-through named a built-in would be the wrong trade. Nothing pins
|
||||
// the shape either: every case in TessellationXfbCaptureScenario builds an explicit
|
||||
// control stage, so a TES-without-TCS test has to come with the fix.
|
||||
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
|
||||
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
|
||||
@@ -3862,14 +4207,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
{
|
||||
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
|
||||
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
|
||||
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
|
||||
// shipping build can be handed by an application. The message is latched all the same
|
||||
// - the pass-through cache is keyed on patchVertices, so a broken template would
|
||||
// otherwise re-report once per distinct patch size.
|
||||
Bool validateThisOne = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
validateThisOne = true;
|
||||
#else
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
validateThisOne = m_enableSpirvValidation;
|
||||
if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
#endif
|
||||
SpirvValidationFailure passthroughFailure{};
|
||||
if (validateThisOne &&
|
||||
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
|
||||
"patchVertices=%u does not validate (%s)",
|
||||
patchVertices, passthroughFailure.message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
|
||||
@@ -202,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
|
||||
@@ -426,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) {
|
||||
@@ -591,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;
|
||||
|
||||
@@ -37,6 +37,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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
|
||||
@@ -360,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) {
|
||||
@@ -496,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') "
|
||||
@@ -1367,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);
|
||||
@@ -1396,6 +1430,83 @@ 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");
|
||||
@@ -1551,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;
|
||||
}
|
||||
|
||||
@@ -1765,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
|
||||
|
||||
@@ -179,7 +179,17 @@ 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
|
||||
@@ -293,6 +303,9 @@ 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
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Math/HalfFloat.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
|
||||
#include "MG_Util/Texture/PixelStoreProcessor.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
@@ -3159,6 +3160,7 @@ void main() {
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||
m_shaderDrawParametersFeatureEnabled,
|
||||
m_unformattedFloatStorageImagesEnabled,
|
||||
m_tessellationAndGeometryPointSizeFeatureEnabled,
|
||||
MG_Config::Features.EnableSpirvValidation,
|
||||
m_updateAfterBindLimits, subgroupPolicy);
|
||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||
@@ -3276,6 +3278,17 @@ void main() {
|
||||
vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr);
|
||||
m_xfbQueryPool = VK_NULL_HANDLE;
|
||||
}
|
||||
if (m_primGenReroutePool != VK_NULL_HANDLE) {
|
||||
vkDestroyQueryPool(m_device, m_primGenReroutePool, nullptr);
|
||||
m_primGenReroutePool = VK_NULL_HANDLE;
|
||||
}
|
||||
m_primGenRerouteActiveSlots.clear();
|
||||
m_primGenRerouteSlotCursor = 0;
|
||||
m_primGenRerouteSlotOpen = false;
|
||||
// Not sticky across renderers: the next bring-up re-decides both (from the
|
||||
// per-process probe memo, so it re-decides without re-probing).
|
||||
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||
m_primGenStreamCountsXfbInactiveDraws = false;
|
||||
m_bufferManager.Shutdown();
|
||||
|
||||
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
|
||||
@@ -4767,16 +4780,43 @@ void main() {
|
||||
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
|
||||
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
|
||||
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
|
||||
// PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask
|
||||
// PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask
|
||||
// - patch vertices, polygon mode, cull face mode, depth func, logic op,
|
||||
// min sample shading
|
||||
// min sample shading, the glSampleMaski word
|
||||
// - front/back stencil ops + compare funcs (ref/mask are dynamic state)
|
||||
// - per draw buffer up to the render pass's colour span: indexed blend enable,
|
||||
// blend factors/equations, indexed colour write mask (broadcast from index 0
|
||||
// when the device lacks independentBlend - the same read the payload does)
|
||||
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
|
||||
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount) const {
|
||||
// The fixed-function sample mask this draw actually gets, and the ONE place that decides it.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 puts SAMPLE_MASK/SAMPLE_MASK_VALUE among the multisample fragment
|
||||
// operations and says they make no change "if MULTISAMPLE is disabled, or if the value of
|
||||
// SAMPLE_BUFFERS is not one" - so on a single-sample draw framebuffer the mask is a no-op.
|
||||
// Vulkan has no such rule: pSampleMask is ANDed with coverage at every rasterizationSamples,
|
||||
// and at one sample that coverage is bit 0 alone. Handing the raw GL word straight through
|
||||
// therefore turned `glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2);` followed by a draw to
|
||||
// the default framebuffer - the ordinary MSAA-render-then-present shape, and what dEQP's
|
||||
// multisample cases leave enabled - into a fully discarded, black draw. All-ones restores
|
||||
// the null-pSampleMask meaning the pipeline had before the mask was plumbed at all.
|
||||
//
|
||||
// SAMPLE_BUFFERS is the load-bearing half: MultisampleEnabled defaults to TRUE, so the
|
||||
// capability check alone would gate nothing. It is here for spec completeness - GL lets
|
||||
// glDisable(GL_MULTISAMPLE) switch the whole step off on a multisample target too.
|
||||
//
|
||||
// Both callers - the payload and ComputePipelineStateHash's memo word - go through this, so
|
||||
// the memo key cannot describe a different mask than the pipeline was built with.
|
||||
Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const {
|
||||
constexpr Uint32 kFullCoverage = 0xffffffffu;
|
||||
if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
|
||||
return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;
|
||||
}
|
||||
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const {
|
||||
// One bulk fetch instead of ~17 per-field accessor calls into MG_State: every
|
||||
// input below is a plain field of RenderStateParameters, and each accessor this
|
||||
// replaces (IsCapabilityEnabled / Get*) is a verified pure read of that same
|
||||
@@ -4795,6 +4835,13 @@ void main() {
|
||||
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
|
||||
capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
|
||||
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
|
||||
// The EFFECTIVE mask enable, not the raw GL bit: at one sample GL says the whole
|
||||
// multisample fragment-operations step makes no change, so the pipeline is built with
|
||||
// full coverage and the memo word has to say so too. Keying on the raw bit here while
|
||||
// the payload gates on the sample count would let one FBO's cached pipeline answer for
|
||||
// another whose sample count reads the mask differently.
|
||||
const Bool sampleMaskEffective = ResolveEffectiveSampleMask(rasterizationSamples) != 0xffffffffu;
|
||||
capabilityBits |= sampleMaskEffective ? 1ull << 10 : 0;
|
||||
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
|
||||
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
|
||||
// versions, so an unhashed float would let a pipeline built at one rate be handed back
|
||||
@@ -4804,6 +4851,13 @@ void main() {
|
||||
std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits));
|
||||
}
|
||||
// glSampleMaski's word, for the same reason glMinSampleShading's bits are hashed above:
|
||||
// this memo compares hashes, not versions, so a mask that moved between two otherwise
|
||||
// identical draws has to key a different pipeline. Hashed unconditionally rather than only
|
||||
// while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and
|
||||
// folding one more word costs nothing on a path that only recomputes when the
|
||||
// pipeline-state version moved.
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ResolveEffectiveSampleMask(rasterizationSamples)));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
|
||||
// The default tessellation levels belong here for the same reason PatchVertices does:
|
||||
// when a program has an evaluation stage and no control stage, both are compiled into the
|
||||
@@ -4927,10 +4981,13 @@ void main() {
|
||||
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount) {
|
||||
m_pipelineStateHash = ComputePipelineStateHash(renderPassEntry.colorAttachmentCount);
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
|
||||
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount);
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount;
|
||||
m_pipelineStateHashSampleCount = renderPassEntry.sampleCount;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
||||
@@ -5199,6 +5256,8 @@ void main() {
|
||||
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
|
||||
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
|
||||
// Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63.
|
||||
.sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu},
|
||||
.subpass = 0,
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
@@ -5533,18 +5592,32 @@ void main() {
|
||||
}
|
||||
}
|
||||
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
||||
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected at
|
||||
// device creation and surfaced in the POST; if a shader actually issues a draw with a SRC1
|
||||
// factor on a device that lacks it, there is no fallback, so hard-fail here at use time
|
||||
// rather than silently mistranslating the blend equation.
|
||||
if (effectiveBlendEnabled && !m_dualSrcBlendFeatureEnabled &&
|
||||
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected
|
||||
// at device creation and surfaced in the POST; there is no fallback that BLENDS correctly,
|
||||
// so a draw that asks for a SRC1 factor on a device without the feature gets the blend
|
||||
// DECLINED - this attachment is baked with blending off and neutral One/Zero factors, and
|
||||
// the loss is logged once. Both the factors AND the enable have to be neutralised:
|
||||
// VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three
|
||||
// siblings forbid a VK_BLEND_FACTOR_SRC1_* in the struct without the feature whatever
|
||||
// blendEnable says, so clearing only the enable would still be invalid pipeline state.
|
||||
// The previous behaviour, throwing, took the whole process down over one unsupported
|
||||
// blend factor; this is defined, survivable and visible in the log, and it matches what
|
||||
// the non-blendable-format arm above already does.
|
||||
if (!m_dualSrcBlendFeatureEnabled &&
|
||||
(IsDualSourceBlendFactor(srcRGB) || IsDualSourceBlendFactor(dstRGB) ||
|
||||
IsDualSourceBlendFactor(srcAlpha) || IsDualSourceBlendFactor(dstAlpha))) {
|
||||
THROW_EXCEPTION(
|
||||
"Dual-source blending (GL_SRC1_* blend factor) was used on color attachment " +
|
||||
std::to_string(i) +
|
||||
", but the Vulkan device does not support the dualSrcBlend feature (see the "
|
||||
"dualSrcBlend row in the driver POST). No fallback exists; the draw cannot proceed.");
|
||||
MGLOG_E_ONCE(
|
||||
"GetOrCreatePipeline: dual-source blending (GL_SRC1_* blend factor) was requested on "
|
||||
"color attachment %u, but the Vulkan device does not support the dualSrcBlend feature "
|
||||
"(see the dualSrcBlend row in the driver POST). Blending is DECLINED on that "
|
||||
"attachment - the fragment's first output is written unblended and the second source "
|
||||
"is dropped (program=%u)",
|
||||
i, program.GetExternalIndex());
|
||||
effectiveBlendEnabled = false;
|
||||
srcRGB = BlendFactor::One;
|
||||
dstRGB = BlendFactor::Zero;
|
||||
srcAlpha = BlendFactor::One;
|
||||
dstAlpha = BlendFactor::Zero;
|
||||
}
|
||||
payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState(
|
||||
effectiveBlendEnabled,
|
||||
@@ -6047,6 +6120,16 @@ void main() {
|
||||
snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch();
|
||||
}
|
||||
const auto& programObj = *programObjPtr;
|
||||
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
|
||||
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
|
||||
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
// A refused program cannot reach here today - the full path refuses before it ever
|
||||
// records a snapshot - but declining the fast path costs one compare and means the
|
||||
// refusal does not depend on that ordering staying true.
|
||||
if (programObj.pointSizeCapabilityUnsupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The pipeline and the vertex-input pre-flight depend on the VAO only through
|
||||
// its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the
|
||||
@@ -6209,10 +6292,13 @@ void main() {
|
||||
// instead of missing forever on a monotonic version. A miss falls through
|
||||
// to the full lookup.
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != snap.renderPassColorCount) {
|
||||
m_pipelineStateHash = ComputePipelineStateHash(snap.renderPassColorCount);
|
||||
m_pipelineStateHashColorCount != snap.renderPassColorCount ||
|
||||
m_pipelineStateHashSampleCount != snap.renderPassSampleCount) {
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount);
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = snap.renderPassColorCount;
|
||||
m_pipelineStateHashSampleCount = snap.renderPassSampleCount;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
const auto memoTransformFlags =
|
||||
@@ -6447,6 +6533,16 @@ void main() {
|
||||
}
|
||||
}
|
||||
const auto& programObj = *resolvedProgramObj;
|
||||
// Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has
|
||||
// no way to know the bound pipeline's last pre-rasterization module lost (or never got)
|
||||
// its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined.
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
// The build already said why, once, naming the program and the stage. Refusing here -
|
||||
// before any pipeline is built from it - is what makes that message a decline rather
|
||||
// than a note attached to invalid usage the driver still receives.
|
||||
if (programObj.pointSizeCapabilityUnsupported) {
|
||||
return false;
|
||||
}
|
||||
// For the snapshot's memoised entry pointer: if anything below inserts into the
|
||||
// program cache (blit/aux program compiles), the epoch moves and the snapshot
|
||||
// stores no pointer for this draw - the fast path then re-looks-up once.
|
||||
@@ -6485,11 +6581,31 @@ void main() {
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 programVersion = program.GetBackendStateVersion();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
// The bind generation alone stopped covering this set the moment ResolveSampledBinding
|
||||
// started asking SamplesAsIncompleteTexture: membership now depends on the effective
|
||||
// sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on
|
||||
// the texture SHAPE, and neither moves the bind generation. A texture that flips
|
||||
// incomplete -> complete under a fixed binding - one glTexParameteri, one
|
||||
// glSamplerParameteri, a BASE_LEVEL/MAX_LEVEL change, or an upload that fills the
|
||||
// chain - would keep replaying the FALLBACK out of this memo, so the real texture
|
||||
// never got its pre-pass sync, its pending-clear materialisation or its sampled-layout
|
||||
// transition, and the descriptor path would then transition it from INSIDE the open
|
||||
// render pass, which the subpass declares no self-dependency for.
|
||||
//
|
||||
// The sampling-resolution generation is exactly the counter for that family and is
|
||||
// deliberately coarse (any texture, any sampler), so this one term covers every input
|
||||
// the predicate reads that the bind generation does not: TextureObjectBase::
|
||||
// BumpShapeVersion and SamplerObject::BumpVersion both bump it, while WHICH sampler
|
||||
// object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind
|
||||
// generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo
|
||||
// already carry.
|
||||
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Bool sampledSetUnchanged =
|
||||
m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId &&
|
||||
m_lastSampledSetProgramVersion == programVersion &&
|
||||
m_lastSampledSetTransformFlags == transformFlags &&
|
||||
m_lastSampledSetBindGeneration == bindGeneration;
|
||||
m_lastSampledSetBindGeneration == bindGeneration &&
|
||||
m_lastSampledSetSamplingGeneration == samplingGeneration;
|
||||
if (!sampledSetUnchanged) {
|
||||
const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(
|
||||
program, programObj, sampledTextures, &m_sampledBindingRecordsScratch);
|
||||
@@ -6499,6 +6615,7 @@ void main() {
|
||||
m_lastSampledSetProgramVersion = programVersion;
|
||||
m_lastSampledSetTransformFlags = transformFlags;
|
||||
m_lastSampledSetBindGeneration = bindGeneration;
|
||||
m_lastSampledSetSamplingGeneration = samplingGeneration;
|
||||
}
|
||||
// Complete a freshly-made LOD decision (see above): its params sum
|
||||
// can only be taken once the sampled set is known. A genuine
|
||||
@@ -6544,9 +6661,21 @@ void main() {
|
||||
}
|
||||
|
||||
auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture);
|
||||
MOBILEGL_ASSERT(textureResource != nullptr,
|
||||
"%s: SyncTextureAndGetDescriptor failed for textureId=%d",
|
||||
__func__, sampledTexture->GetExternalIndex());
|
||||
if (textureResource == nullptr) {
|
||||
// SyncTextureAndGetDescriptor has a real failure channel - an incomplete or
|
||||
// otherwise unbackable texture declines and returns nullptr with its own log
|
||||
// line - and the assert that used to be the only guard here is compiled out of
|
||||
// every build past DEBUG. The next line dereferenced it, so a sampler left
|
||||
// pointing at a texture GL calls incomplete was a SIGSEGV inside SetupDraw
|
||||
// rather than a degraded draw. Leave the slot null and carry on: the descriptor
|
||||
// resolve substitutes the fallback texture for exactly these bindings
|
||||
// (ResolveSamplerDescriptor's SamplesAsIncompleteTexture branch), and the fast
|
||||
// path at the top of SetupDraw already treats a null resource as "re-resolve".
|
||||
MGLOG_E_ONCE("SetupDraw: no texture resource for sampled textureId=%d; leaving the binding to the "
|
||||
"descriptor resolve's fallback",
|
||||
sampledTexture->GetExternalIndex());
|
||||
continue;
|
||||
}
|
||||
sampledResources[sampledIndex] = textureResource;
|
||||
MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)",
|
||||
sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout),
|
||||
@@ -6610,9 +6739,14 @@ void main() {
|
||||
MOBILEGL_ASSERT(ready, "%s: TransitionTextureForSampling failed for textureId=%d",
|
||||
__func__, sampledTexture->GetExternalIndex());
|
||||
auto* transitionedResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture);
|
||||
MOBILEGL_ASSERT(transitionedResource != nullptr,
|
||||
"%s: post-transition SyncTextureAndGetDescriptor failed for textureId=%d",
|
||||
__func__, sampledTexture->GetExternalIndex());
|
||||
if (transitionedResource == nullptr) {
|
||||
// Same declined-sync channel as the first loop, and the same reason not to
|
||||
// dereference it: StampResourceRecordingUse below takes a reference.
|
||||
MGLOG_E_ONCE("SetupDraw: no texture resource after transitioning sampled textureId=%d; leaving the "
|
||||
"binding to the descriptor resolve's fallback",
|
||||
sampledTexture->GetExternalIndex());
|
||||
continue;
|
||||
}
|
||||
// Pre-pass stream bookkeeping: the draw about to be recorded reads
|
||||
// this image, so later out-of-pass work on it can no longer jump
|
||||
// ahead of the recording.
|
||||
@@ -6780,6 +6914,7 @@ void main() {
|
||||
snap.drawUsesDepthStencil = drawUsesDepthStencil;
|
||||
snap.renderPassExtent = renderPassEntry->extent;
|
||||
snap.renderPassColorCount = renderPassEntry->colorAttachmentCount;
|
||||
snap.renderPassSampleCount = renderPassEntry->sampleCount;
|
||||
snap.pipeline = pipeline;
|
||||
// The layout identity the fast path's aux-memo compare answers against.
|
||||
// A memo hit here, not a rebuild: the pre-flight above resolved this
|
||||
@@ -7579,7 +7714,8 @@ void main() {
|
||||
|
||||
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue) {
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout) {
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
|
||||
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
|
||||
@@ -7592,7 +7728,11 @@ void main() {
|
||||
|
||||
VkAttachmentDescription colorAttachment{};
|
||||
colorAttachment.format = resource->format;
|
||||
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// The image's own count, not a hardcoded one: a render-pass attachment must match the
|
||||
// image it is given (VUID-VkFramebufferCreateInfo-pAttachments-00880), and this helper is
|
||||
// now also the multisample path - a multisample image carries no TRANSFER_DST usage, so a
|
||||
// load-op clear is the only legal way to clear it at all.
|
||||
colorAttachment.samples = resource->sampleCount;
|
||||
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -7600,7 +7740,7 @@ void main() {
|
||||
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
// Hand the slice back in the layout the caller already tracks for the whole image, so its
|
||||
// closing barrier stays truthful and resource->layout is never touched from in here.
|
||||
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
colorAttachment.finalLayout = finalLayout;
|
||||
|
||||
VkAttachmentReference colorRef{};
|
||||
colorRef.attachment = 0;
|
||||
@@ -7668,9 +7808,26 @@ void main() {
|
||||
"MaterializePendingClearForTexture requires no active render pass on the target buffer");
|
||||
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
|
||||
MOBILEGL_ASSERT(resource != nullptr,
|
||||
"MaterializePendingClearForTexture: SyncTextureAndGetDescriptor failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
if (resource == nullptr) {
|
||||
// Declined sync (an incomplete texture, say). Nothing to clear into, and every line
|
||||
// below dereferences this - the assert that used to stand here is compiled out of
|
||||
// every build past DEBUG.
|
||||
MGLOG_E_ONCE("MaterializePendingClearForTexture: no texture resource for textureId=%d; the queued clears "
|
||||
"stay queued",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
// A multisample image is not a transfer target: SyncTextureResource deliberately withholds
|
||||
// TRANSFER_DST/TRANSFER_SRC from every one of them, so the vkCmdClearColorImage below -
|
||||
// and the TRANSFER_DST transition ahead of it - are invalid usage
|
||||
// (VUID-vkCmdClearColorImage-image-00002) on exactly the shape a
|
||||
// glClearBufferfv-then-sample sequence produces. Clear it the one way that is legal at
|
||||
// any sample count instead: a throwaway render pass whose whole content is its load-op
|
||||
// clear, which is also what the 3D-slice case below already does.
|
||||
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) {
|
||||
return MaterializeMultisamplePendingClear(commandBuffer, texture, *resource, pendingClears);
|
||||
}
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
@@ -7810,6 +7967,77 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
const Vector<PendingClearEntry>& pendingClears) {
|
||||
// Colour only. GL can queue a depth/stencil clear on a multisample texture too, and the
|
||||
// load-op idiom would serve it just as well, but this helper attaches its view as a
|
||||
// COLOUR attachment; declining is honest and leaves the queue intact for a later path.
|
||||
if ((resource.aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d is a multisample depth/stencil texture; "
|
||||
"its queued clear cannot be materialised out of a render pass yet",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool allCleared = true;
|
||||
for (const auto& pendingClear : pendingClears) {
|
||||
if (pendingClear.key.mipLevel >= resource.mipLevels) {
|
||||
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d pending clear mip=%u out of range %u",
|
||||
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource.mipLevels);
|
||||
allCleared = false;
|
||||
continue;
|
||||
}
|
||||
auto clearPayload = pendingClear.payload;
|
||||
PreCompensateSrgbClearColor(clearPayload, resource.format);
|
||||
VkClearValue clearValue{};
|
||||
clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture));
|
||||
|
||||
// A multisample texture has exactly one level and, for the 2D target, one layer; the
|
||||
// array target's layers are cleared one at a time, which is what this helper's
|
||||
// per-layer view gives us.
|
||||
const Uint32 firstLayer = pendingClear.key.baseArrayLayer;
|
||||
const Uint32 layerCount = std::max(pendingClear.key.layerCount, 1u);
|
||||
for (Uint32 layer = firstLayer; layer < firstLayer + layerCount; ++layer) {
|
||||
if (layer >= resource.arrayLayers) break;
|
||||
// COLOR_ATTACHMENT_OPTIMAL, not TRANSFER_DST: the image never has transfer usage,
|
||||
// and a render target is where it came from and where it is going.
|
||||
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, layer,
|
||||
clearValue, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)) {
|
||||
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d layer %u could not be cleared",
|
||||
texture.GetExternalIndex(), layer);
|
||||
allCleared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allCleared) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The load-op clear left every touched layer in COLOR_ATTACHMENT_OPTIMAL (each pass's
|
||||
// finalLayout), so that - not the tracked layout on entry - is what the closing barrier
|
||||
// has to start from.
|
||||
// TransitionImageLayout takes the tracked layout by reference and updates it, so seeding
|
||||
// it is both how the barrier learns its source and how resource->layout ends up right.
|
||||
resource.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
const Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, resource.image, resource.layout,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
||||
resource.aspect, 0, resource.mipLevels);
|
||||
if (!ok) {
|
||||
MGLOG_E_ONCE("MaterializeMultisamplePendingClear: failed to transition textureId=%d to the sampled layout",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_clearManager->PopPendingClear(&texture);
|
||||
MGLOG_D("MaterializeMultisamplePendingClear: textureId=%d pending clear materialised through a load-op pass",
|
||||
texture.GetExternalIndex());
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializePendingClearForRenderbuffer(
|
||||
VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {
|
||||
if (renderbuffer == nullptr) {
|
||||
@@ -10487,19 +10715,30 @@ void main() {
|
||||
"GetTexImage: failed to materialize pending clear for textureId=%d",
|
||||
textureObject->GetExternalIndex());
|
||||
|
||||
// WHICH FACE the caller asked for. glGetTexImage names one face of a cube map through the
|
||||
// TARGET token (GL_TEXTURE_CUBE_MAP_NEGATIVE_X and friends, GL 4.6 core 8.11), and a cube
|
||||
// map's six faces are its VkImage's six ARRAY LAYERS - so unless the token is turned into a
|
||||
// baseArrayLayer, every face token reads layer 0 and the whole cube answers as +X. The
|
||||
// image's own target cannot supply this: a plain GL_TEXTURE_CUBE_MAP is not an array target,
|
||||
// so the layer arithmetic below leaves it at one layer starting at zero, which is precisely
|
||||
// the layer this face index has to displace. Same conversion, same reason, as
|
||||
// VkClearManager's / VkRenderPassManager's ResolveAttachmentBaseArrayLayer, which resolve an
|
||||
// ATTACHMENT's face; this is the readback's copy of it. Zero for every other target,
|
||||
// including a cube map ARRAY - that one arrives as TextureUploadTarget::CubeMapArray with
|
||||
// its layer-faces already counted in the level's z, not as a face token.
|
||||
const Bool isCubeFaceTarget = textureUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
textureUploadTarget <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
const Int glCubeFaceLayer = isCubeFaceTarget
|
||||
? static_cast<Int>(textureUploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX)
|
||||
: 0;
|
||||
|
||||
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) {
|
||||
const auto levelSize =
|
||||
textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
const Bool isCubeFace = textureUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
textureUploadTarget <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
// Storage space: `resource` is the storage texture's, so a view's level and
|
||||
// layer have to be shifted into its numbering (see ToStorageMipLevel).
|
||||
const Int glArrayLayer = isCubeFace
|
||||
? static_cast<Int>(textureUploadTarget) -
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX)
|
||||
: 0;
|
||||
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glArrayLayer);
|
||||
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||
const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
||||
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
||||
@@ -10594,7 +10833,10 @@ void main() {
|
||||
// Storage space, as above: a texture view reads its own level 0 out of whichever level
|
||||
// and layer of the parent it opened onto.
|
||||
copyRegion.imageSubresource.mipLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
copyRegion.imageSubresource.baseArrayLayer = ToStorageArrayLayer(textureObject.get(), 0);
|
||||
// glCubeFaceLayer, not 0: the cube face the target token named (see above). Non-zero for
|
||||
// exactly one shape - a plain cube map read one face at a time - and layerCount is 1 there,
|
||||
// so the copy stays inside the six layers the image has.
|
||||
copyRegion.imageSubresource.baseArrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
||||
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
||||
@@ -10919,6 +11161,19 @@ void main() {
|
||||
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
// The bound pipeline's last pre-rasterization stage has to have been declared with Xfb
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128). Everything above this line reads GL
|
||||
// state, which cannot answer that: a program can be built as a capture variant and still
|
||||
// end up with a module carrying no Xfb mode - the clip/XFB validation backstop rewinding
|
||||
// past the decoration, or XfbCaptureDecoratePass resolving none of the requested varyings
|
||||
// and changing nothing. Declining the span leaves the capture buffers untouched, which is
|
||||
// the same nothing the driver would have written, without the undefined behaviour.
|
||||
if (m_currentDrawXfbCaptureDeclined) {
|
||||
MGLOG_E_ONCE("BeginXfbCaptureForDraw: declining the capture span - the bound program's last "
|
||||
"pre-rasterization stage carries no Xfb execution mode, so recording one would be "
|
||||
"undefined behaviour rather than a capture");
|
||||
return false;
|
||||
}
|
||||
const SizeT bufferCount = std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4);
|
||||
if (bufferCount == 0) {
|
||||
return false;
|
||||
@@ -11056,7 +11311,7 @@ void main() {
|
||||
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
||||
|
||||
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
||||
BeginXfbQueryForDraw(commandBuffer);
|
||||
BeginXfbQueryForDraw(commandBuffer, xfbActive);
|
||||
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
||||
vkCmdDraw(commandBuffer,
|
||||
payload.params.vertexCount,
|
||||
@@ -11142,23 +11397,81 @@ void main() {
|
||||
}
|
||||
s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots);
|
||||
}
|
||||
// The reroute pool, on the first GENERATED span that needs it. A creation
|
||||
// failure disarms rather than failing the capture: the stream path still
|
||||
// answers (with the driver's defect), which beats answering nothing.
|
||||
if (kind == 1 && m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||
m_primGenReroutePool == VK_NULL_HANDLE) {
|
||||
VkQueryPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
poolInfo.queryCount = kXfbQuerySlots;
|
||||
if (m_primGenRerouteKind == MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt) {
|
||||
// The query Vulkan defines for this GL target; counts vertex stream 0
|
||||
// when begun with plain vkCmdBeginQuery.
|
||||
poolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT;
|
||||
} else {
|
||||
poolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
|
||||
// The clipping-stage INVOCATION counter: one per primitive reaching
|
||||
// primitive clipping (GL's CLIPPING_INPUT_PRIMITIVES) - post-tess/GS,
|
||||
// pre-clip, and per spec still counted under rasterizer discard, which
|
||||
// is exactly the set GL_PRIMITIVES_GENERATED is defined over. The
|
||||
// stage's OUTPUT count (CLIPPING_PRIMITIVES_BIT) would be wrong:
|
||||
// clipping may drop or split primitives.
|
||||
poolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT;
|
||||
}
|
||||
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_primGenReroutePool) != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("StartXfbQueryCapture: reroute pool creation failed; the "
|
||||
"PRIMITIVES_GENERATED reroute is disarmed and XFB-inactive draws keep "
|
||||
"the stream query");
|
||||
m_primGenReroutePool = VK_NULL_HANDLE;
|
||||
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||
} else {
|
||||
s_vkResetQueryPool(m_device, m_primGenReroutePool, 0, kXfbQuerySlots);
|
||||
}
|
||||
}
|
||||
m_xfbQueryActiveSlots[kind].clear();
|
||||
m_xfbQueryCaptureActive[kind] = true;
|
||||
if (kind == 1) {
|
||||
m_primGenRerouteActiveSlots.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots) {
|
||||
Bool VulkanRenderer::ArePausedDrawsGpuCounted() const {
|
||||
// Exactly the gate BeginXfbQueryForDraw applies per draw, so a span told "armed"
|
||||
// really does get a reroute slot for every draw with no open capture - a paused
|
||||
// span's draws included.
|
||||
const Bool rerouteArmed = m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||
m_primGenReroutePool != VK_NULL_HANDLE;
|
||||
// Otherwise the paused draw takes a stream slot, which is an exact count of it
|
||||
// on a driver the probe measured as counting capture-less draws.
|
||||
return rerouteArmed || m_primGenStreamCountsXfbInactiveDraws;
|
||||
}
|
||||
|
||||
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots,
|
||||
Vector<Uint32>& outRerouteSlots) {
|
||||
if (kind > 1) {
|
||||
return;
|
||||
}
|
||||
outSlots = Move(m_xfbQueryActiveSlots[kind]);
|
||||
m_xfbQueryActiveSlots[kind].clear();
|
||||
m_xfbQueryCaptureActive[kind] = false;
|
||||
outRerouteSlots.clear();
|
||||
if (kind == 1) {
|
||||
outRerouteSlots = Move(m_primGenRerouteActiveSlots);
|
||||
m_primGenRerouteActiveSlots.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives) {
|
||||
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||
Bool wantGenerated, Uint64& outPrimitives) {
|
||||
outPrimitives = 0;
|
||||
if (slots.empty() || m_xfbQueryPool == VK_NULL_HANDLE) {
|
||||
const Bool haveStreamSlots = !slots.empty() && m_xfbQueryPool != VK_NULL_HANDLE;
|
||||
// Reroute slots only ever accumulate the GENERATED target (see
|
||||
// BeginXfbQueryForDraw); WRITTEN never opens one.
|
||||
const Bool haveRerouteSlots =
|
||||
wantGenerated && !rerouteSlots.empty() && m_primGenReroutePool != VK_NULL_HANDLE;
|
||||
if (!haveStreamSlots && !haveRerouteSlots) {
|
||||
return true;
|
||||
}
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
@@ -11170,44 +11483,108 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const Uint32 slot : slots) {
|
||||
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||
const VkResult result =
|
||||
vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair),
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
||||
if (result == VK_SUCCESS) {
|
||||
outPrimitives += pair[wantGenerated ? 1 : 0];
|
||||
if (haveStreamSlots) {
|
||||
for (const Uint32 slot : slots) {
|
||||
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||
const VkResult result =
|
||||
vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair),
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
||||
if (result == VK_SUCCESS) {
|
||||
outPrimitives += pair[wantGenerated ? 1 : 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (haveRerouteSlots) {
|
||||
for (const Uint32 slot : rerouteSlots) {
|
||||
// Both reroute pool kinds answer one 64-bit primitive count per slot.
|
||||
Uint64 generated = 0;
|
||||
const VkResult result = vkGetQueryPoolResults(
|
||||
m_device, m_primGenReroutePool, slot, 1, sizeof(generated), &generated,
|
||||
sizeof(generated), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
|
||||
if (result == VK_SUCCESS) {
|
||||
outPrimitives += generated;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer) {
|
||||
void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive) {
|
||||
m_xfbQuerySlotOpen = false;
|
||||
m_primGenRerouteSlotOpen = false;
|
||||
if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
const Uint32 slot = m_xfbQuerySlotCursor;
|
||||
m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots;
|
||||
// Slots are never host-reset at read time (both GL targets may reference one
|
||||
// slot); recycle them here instead.
|
||||
s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1);
|
||||
s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0);
|
||||
for (Uint32 kind = 0; kind < 2; ++kind) {
|
||||
if (m_xfbQueryCaptureActive[kind]) {
|
||||
m_xfbQueryActiveSlots[kind].push_back(slot);
|
||||
// Every draw with no OPEN capture is the stream query's silent case, and that
|
||||
// includes a draw made while the GL span is merely PAUSED (the pause closes the
|
||||
// capture, so BeginXfbCaptureForDraw already answered false for it). Paused
|
||||
// draws are rerouted like any other: the frontend's CPU paused-primitive
|
||||
// counter cannot stand in for them - it is written by only 3 of the ~15 draw
|
||||
// entry points (never the instanced, indirect or multi-draw ones) and answers 0
|
||||
// for GL_PATCHES by design, since the tessellator's amplification is not
|
||||
// knowable on the CPU - which is exactly the CTS's shape. Double counting is
|
||||
// prevented on the other side instead: a GENERATED span opened while this
|
||||
// reroute is armed ignores that CPU counter entirely (see
|
||||
// ArePausedDrawsGpuCounted and DirectVulkan.cpp's XfbGenerated resolve), so
|
||||
// every XFB-inactive draw in the span is priced exactly once, by this pool.
|
||||
const Bool rerouteGenerated = m_xfbQueryCaptureActive[1] &&
|
||||
m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
|
||||
m_primGenReroutePool != VK_NULL_HANDLE && !xfbActive;
|
||||
// The stream slot stays for WRITTEN whatever the reroute does (with capture
|
||||
// inactive its primitivesWritten is 0, which is the correct WRITTEN answer),
|
||||
// and for GENERATED wherever this draw is not rerouted - so one GL query span
|
||||
// may accumulate stream slots (XFB-active draws) and reroute slots
|
||||
// (XFB-inactive draws) side by side.
|
||||
const Bool wantStreamSlot =
|
||||
m_xfbQueryCaptureActive[0] || (m_xfbQueryCaptureActive[1] && !rerouteGenerated);
|
||||
if (wantStreamSlot) {
|
||||
const Uint32 slot = m_xfbQuerySlotCursor;
|
||||
m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots;
|
||||
// Slots are never host-reset at read time (both GL targets may reference one
|
||||
// slot); recycle them here instead.
|
||||
s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1);
|
||||
s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0);
|
||||
if (m_xfbQueryCaptureActive[0]) {
|
||||
m_xfbQueryActiveSlots[0].push_back(slot);
|
||||
}
|
||||
if (m_xfbQueryCaptureActive[1] && !rerouteGenerated) {
|
||||
m_xfbQueryActiveSlots[1].push_back(slot);
|
||||
}
|
||||
m_xfbQuerySlotOpen = true;
|
||||
m_xfbQueryOpenSlot = slot;
|
||||
}
|
||||
if (rerouteGenerated) {
|
||||
// Latched at INFO on purpose: it is the pinned integration lane's arming
|
||||
// observable (the shape UnlocatedIoBlockScenario asserts), and the builds
|
||||
// CI runs compile INFO in.
|
||||
MGLOG_I_ONCE("PRIMITIVES_GENERATED reroute engaged: an XFB-inactive draw accumulates "
|
||||
"through the %s pool",
|
||||
m_primGenRerouteKind ==
|
||||
MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||
? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT"
|
||||
: "clipping-invocations statistics");
|
||||
const Uint32 slot = m_primGenRerouteSlotCursor;
|
||||
m_primGenRerouteSlotCursor = (m_primGenRerouteSlotCursor + 1) % kXfbQuerySlots;
|
||||
// Same recycle-at-begin discipline as the stream pool. Both pool kinds
|
||||
// are begun with plain vkCmdBeginQuery (a PRIMITIVES_GENERATED_EXT
|
||||
// query begun this way counts vertex stream 0).
|
||||
s_vkResetQueryPool(m_device, m_primGenReroutePool, slot, 1);
|
||||
vkCmdBeginQuery(commandBuffer, m_primGenReroutePool, slot, 0);
|
||||
m_primGenRerouteActiveSlots.push_back(slot);
|
||||
m_primGenRerouteSlotOpen = true;
|
||||
m_primGenRerouteOpenSlot = slot;
|
||||
}
|
||||
m_xfbQuerySlotOpen = true;
|
||||
m_xfbQueryOpenSlot = slot;
|
||||
}
|
||||
|
||||
void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) {
|
||||
if (!m_xfbQuerySlotOpen) {
|
||||
return;
|
||||
if (m_xfbQuerySlotOpen) {
|
||||
s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0);
|
||||
m_xfbQuerySlotOpen = false;
|
||||
}
|
||||
if (m_primGenRerouteSlotOpen) {
|
||||
vkCmdEndQuery(commandBuffer, m_primGenReroutePool, m_primGenRerouteOpenSlot);
|
||||
m_primGenRerouteSlotOpen = false;
|
||||
}
|
||||
s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0);
|
||||
m_xfbQuerySlotOpen = false;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
|
||||
@@ -11257,7 +11634,7 @@ void main() {
|
||||
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
||||
|
||||
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
|
||||
BeginXfbQueryForDraw(commandBuffer);
|
||||
BeginXfbQueryForDraw(commandBuffer, xfbActive);
|
||||
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
|
||||
vkCmdDrawIndexed(commandBuffer,
|
||||
payload.params.indexCount,
|
||||
@@ -12971,12 +13348,24 @@ void main() {
|
||||
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
|
||||
// out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight
|
||||
// repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules.
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS leaves it off to measure or dodge its GPU cost.
|
||||
deviceFeatures.robustBufferAccess = MG_Config::Features.DisableRobustBufferAccess
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS leaves it off to measure or dodge its GPU cost.
|
||||
deviceFeatures.robustBufferAccess = MG_Config::Features.MagmaDisableRobustBufferAccess
|
||||
? VK_FALSE
|
||||
: supportedDeviceFeatures.robustBufferAccess;
|
||||
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
||||
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
|
||||
// gl_PointSize is an ORDINARY per-vertex output in desktop GL - a tessellation
|
||||
// evaluation or geometry shader may write it, and a program may capture it by name -
|
||||
// but in Vulkan the PointSize built-in is only usable from those two stages when this
|
||||
// feature is on (VUID-RuntimeSpirv-PointSize-06439; SPIR-V spells the requirement as
|
||||
// the TessellationPointSize / GeometryPointSize capabilities, which glslang emits from
|
||||
// any such write). Left off, every one of those programs is invalid usage that a lenient
|
||||
// driver silently gives an undefined point size and a strict one faults on. Nothing here
|
||||
// asks for it speculatively: the feature is taken only where the device advertises it.
|
||||
deviceFeatures.shaderTessellationAndGeometryPointSize =
|
||||
supportedDeviceFeatures.shaderTessellationAndGeometryPointSize;
|
||||
m_tessellationAndGeometryPointSizeFeatureEnabled =
|
||||
deviceFeatures.shaderTessellationAndGeometryPointSize == VK_TRUE;
|
||||
// Sampled-read barriers may only name the shader stages whose device feature is
|
||||
// actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the
|
||||
// mask is assembled here, next to the feature decision, and handed to consumers.
|
||||
@@ -13078,6 +13467,13 @@ void main() {
|
||||
// occlusion result still satisfies any-samples-style consumers.
|
||||
deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise;
|
||||
m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE;
|
||||
m_tessellationShaderFeatureEnabled = deviceFeatures.tessellationShader == VK_TRUE;
|
||||
// Backs the GL_PRIMITIVES_GENERATED reroute's statistics tier (see the
|
||||
// m_primGenReroute* members): a VK_QUERY_TYPE_PIPELINE_STATISTICS pool may only
|
||||
// be created with this feature enabled. Enabled wherever the device has it - the
|
||||
// feature alone costs nothing; pools exist only where the reroute is armed.
|
||||
deviceFeatures.pipelineStatisticsQuery = supportedDeviceFeatures.pipelineStatisticsQuery;
|
||||
m_pipelineStatisticsQueryFeatureEnabled = deviceFeatures.pipelineStatisticsQuery == VK_TRUE;
|
||||
|
||||
VkDeviceCreateInfo deviceCreateInfo{};
|
||||
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||
@@ -13335,13 +13731,13 @@ void main() {
|
||||
subgroupPropertyQuery.pNext = &subgroupProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
|
||||
// Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the
|
||||
// MOBILEGL_DISABLE_SUBGROUP escape hatch, so the module lowerings can never
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP escape hatch, so the module lowerings can never
|
||||
// disagree with the advertised capabilities.
|
||||
const Bool usableSubgroups =
|
||||
subgroupProperties.subgroupSize > 0 &&
|
||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
|
||||
if (usableSubgroups && !MG_Config::Features.DisableSubgroup) {
|
||||
if (usableSubgroups && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
m_nativeSubgroupSize = subgroupProperties.subgroupSize;
|
||||
m_nativeSubgroupSupported = true;
|
||||
}
|
||||
@@ -13405,6 +13801,38 @@ void main() {
|
||||
MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME);
|
||||
}
|
||||
}
|
||||
// VK_EXT_primitives_generated_query - the query Vulkan defines for GL's
|
||||
// GL_PRIMITIVES_GENERATED precisely because the stream query above needs no
|
||||
// capture by spec but drivers disagree. Taken with BOTH the base feature and the
|
||||
// rasterizer-discard feature or not at all: without the latter, a discarding draw
|
||||
// inside the query is invalid usage, and GL applications toggle discard freely.
|
||||
// Only the PRIMITIVES_GENERATED reroute consumes it (see ArmPrimGenReroute).
|
||||
m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||
m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT primitivesGeneratedQueryFeatures{};
|
||||
primitivesGeneratedQueryFeatures.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||
if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME) &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &primitivesGeneratedQueryFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
if (primitivesGeneratedQueryFeatures.primitivesGeneratedQuery == VK_TRUE &&
|
||||
primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) {
|
||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
||||
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME);
|
||||
}
|
||||
primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithNonZeroStreams = VK_FALSE;
|
||||
primitivesGeneratedQueryFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &primitivesGeneratedQueryFeatures;
|
||||
m_primitivesGeneratedQueryFeatureEnabled = true;
|
||||
m_primitivesGeneratedQueryDiscardFeatureEnabled = true;
|
||||
MGLOG_I("Enabled optional device extension: %s",
|
||||
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME);
|
||||
}
|
||||
}
|
||||
// VK_EXT_provoking_vertex. Two independent features live behind one extension:
|
||||
// provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and
|
||||
// the input-assembler capture order.
|
||||
@@ -13754,6 +14182,123 @@ void main() {
|
||||
m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f;
|
||||
MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)",
|
||||
m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs);
|
||||
|
||||
// Last, because it records on m_graphicsQueue: decide the PRIMITIVES_GENERATED
|
||||
// reroute for XFB-inactive draws. Nothing else has touched the queue yet.
|
||||
ArmPrimGenReroute();
|
||||
}
|
||||
|
||||
void VulkanRenderer::ArmPrimGenReroute() {
|
||||
using namespace MG_Util::SelfTest;
|
||||
m_primGenRerouteKind = PrimGenRerouteKind::None;
|
||||
const MG_Config::QuirkOverride overrideSetting = MG_Config::Features.MagmaPrimGenQueryReroute;
|
||||
// Without stream queries the GENERATED path never opens a slot at all, so
|
||||
// there is nothing to reroute - whatever the override says.
|
||||
if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled) {
|
||||
return;
|
||||
}
|
||||
const Bool primitivesGeneratedQueryUsable =
|
||||
m_primitivesGeneratedQueryFeatureEnabled && m_primitivesGeneratedQueryDiscardFeatureEnabled;
|
||||
PrimitivesGeneratedNoXfbVerdict verdict = PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||
// The probe only matters under Auto (ForceOn bypasses the verdict, ForceOff
|
||||
// never asks), and the answer is a device property - so it is memoized per
|
||||
// process rather than re-paid on every renderer recreation.
|
||||
if (overrideSetting == MG_Config::QuirkOverride::Auto) {
|
||||
static const PrimitivesGeneratedNoXfbMeasurement s_measurement = [&]() {
|
||||
PrimitivesGeneratedNoXfbProbeContext probeContext;
|
||||
probeContext.device = m_device;
|
||||
probeContext.queue = m_graphicsQueue;
|
||||
probeContext.queueFamilyIndex =
|
||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
||||
probeContext.transformFeedbackQueriesUsable = m_xfbQueriesSupported;
|
||||
probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable;
|
||||
probeContext.pipelineStatisticsEnabled = m_pipelineStatisticsQueryFeatureEnabled;
|
||||
probeContext.tessellationEnabled = m_tessellationShaderFeatureEnabled;
|
||||
auto& fns = probeContext.fns;
|
||||
fns.vkCreateCommandPool = vkCreateCommandPool;
|
||||
fns.vkDestroyCommandPool = vkDestroyCommandPool;
|
||||
fns.vkAllocateCommandBuffers = vkAllocateCommandBuffers;
|
||||
fns.vkBeginCommandBuffer = vkBeginCommandBuffer;
|
||||
fns.vkEndCommandBuffer = vkEndCommandBuffer;
|
||||
fns.vkCreateQueryPool = vkCreateQueryPool;
|
||||
fns.vkDestroyQueryPool = vkDestroyQueryPool;
|
||||
fns.vkCmdResetQueryPool = vkCmdResetQueryPool;
|
||||
fns.vkCmdBeginQuery = vkCmdBeginQuery;
|
||||
fns.vkCmdEndQuery = vkCmdEndQuery;
|
||||
fns.vkCmdBeginQueryIndexedEXT = s_vkCmdBeginQueryIndexedEXT;
|
||||
fns.vkCmdEndQueryIndexedEXT = s_vkCmdEndQueryIndexedEXT;
|
||||
fns.vkCreateRenderPass = vkCreateRenderPass;
|
||||
fns.vkDestroyRenderPass = vkDestroyRenderPass;
|
||||
fns.vkCreateFramebuffer = vkCreateFramebuffer;
|
||||
fns.vkDestroyFramebuffer = vkDestroyFramebuffer;
|
||||
fns.vkCmdBeginRenderPass = vkCmdBeginRenderPass;
|
||||
fns.vkCmdEndRenderPass = vkCmdEndRenderPass;
|
||||
fns.vkCreateShaderModule = vkCreateShaderModule;
|
||||
fns.vkDestroyShaderModule = vkDestroyShaderModule;
|
||||
fns.vkCreatePipelineLayout = vkCreatePipelineLayout;
|
||||
fns.vkDestroyPipelineLayout = vkDestroyPipelineLayout;
|
||||
fns.vkCreateGraphicsPipelines = vkCreateGraphicsPipelines;
|
||||
fns.vkDestroyPipeline = vkDestroyPipeline;
|
||||
fns.vkCmdBindPipeline = vkCmdBindPipeline;
|
||||
fns.vkCmdDraw = vkCmdDraw;
|
||||
fns.vkCreateFence = vkCreateFence;
|
||||
fns.vkDestroyFence = vkDestroyFence;
|
||||
fns.vkQueueSubmit = vkQueueSubmit;
|
||||
fns.vkWaitForFences = vkWaitForFences;
|
||||
fns.vkGetQueryPoolResults = vkGetQueryPoolResults;
|
||||
fns.vkDeviceWaitIdle = vkDeviceWaitIdle;
|
||||
return RunPrimitivesGeneratedNoXfbProbe(probeContext);
|
||||
}();
|
||||
verdict = EvaluatePrimitivesGeneratedNoXfbVerdict(s_measurement);
|
||||
if (s_measurement.fenceWaitTimedOut) {
|
||||
// The probe's submission never signaled within its bound, so it left its
|
||||
// command pool, query pools, render pass, framebuffer, shader modules,
|
||||
// pipeline layout, pipelines and fence alive on purpose. This device is the
|
||||
// renderer's own and outlives them, so nothing here may destroy them or
|
||||
// wait the device idle - the queue may still be executing that submission,
|
||||
// and an idle wait is the hang the bound exists to prevent. They leak for
|
||||
// the process's life; a device this sick has bigger problems.
|
||||
MGLOG_W("PRIMITIVES_GENERATED probe timed out waiting on its own submission (%s); its "
|
||||
"Vulkan objects are deliberately leaked and XFB-inactive draws keep the stream "
|
||||
"query", s_measurement.failureReason.c_str());
|
||||
} else if (!s_measurement.ran) {
|
||||
MGLOG_W("PRIMITIVES_GENERATED probe did not run (%s); XFB-inactive draws keep the "
|
||||
"stream query", s_measurement.failureReason.c_str());
|
||||
} else {
|
||||
const auto logShape = [](const char* name,
|
||||
const MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement&
|
||||
shape) {
|
||||
MGLOG_I("PRIMITIVES_GENERATED probe %s: drawn=%d stream=%llu/%llu pgq=%llu(%d) "
|
||||
"stat=%llu(%d)",
|
||||
name, shape.drawn ? 1 : 0,
|
||||
static_cast<unsigned long long>(shape.streamGenerated),
|
||||
static_cast<unsigned long long>(shape.expectedPrimitives),
|
||||
static_cast<unsigned long long>(shape.primitivesGeneratedExt),
|
||||
shape.primitivesGeneratedExtMeasured ? 1 : 0,
|
||||
static_cast<unsigned long long>(shape.statisticsClippingInput),
|
||||
shape.statisticsMeasured ? 1 : 0);
|
||||
};
|
||||
logShape("triangles", s_measurement.trianglesPlain);
|
||||
logShape("triangles+discard", s_measurement.trianglesDiscard);
|
||||
logShape("patches+discard", s_measurement.patchesDiscard);
|
||||
}
|
||||
}
|
||||
// A driver whose stream query counts capture-less draws counts a PAUSED span's
|
||||
// draws through the stream slot they take, so that span's result must not have
|
||||
// the frontend's CPU paused counter added on top of it either (the pre-reroute
|
||||
// accounting did exactly that, double counting every paused draw the CPU could
|
||||
// price). Measured, not assumed: the forced arms never ask the probe and leave
|
||||
// this false.
|
||||
m_primGenStreamCountsXfbInactiveDraws = verdict == PrimitivesGeneratedNoXfbVerdict::StreamCounts;
|
||||
m_primGenRerouteKind = ChoosePrimitivesGeneratedReroute(
|
||||
overrideSetting, verdict, primitivesGeneratedQueryUsable, m_pipelineStatisticsQueryFeatureEnabled);
|
||||
if (m_primGenRerouteKind != PrimGenRerouteKind::None) {
|
||||
MGLOG_I("PRIMITIVES_GENERATED for XFB-inactive draws will accumulate through a %s pool%s",
|
||||
m_primGenRerouteKind == PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||
? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT"
|
||||
: "clipping-invocations pipeline-statistics",
|
||||
overrideSetting == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::CreateAllocator() {
|
||||
|
||||
@@ -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,14 @@ 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
|
||||
@@ -709,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;
|
||||
@@ -768,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
|
||||
@@ -812,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
|
||||
@@ -823,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
|
||||
@@ -921,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
|
||||
@@ -1279,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
|
||||
|
||||
@@ -146,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;
|
||||
}
|
||||
}
|
||||
@@ -172,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:
|
||||
@@ -381,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;
|
||||
|
||||
@@ -64,21 +64,43 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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 has to ACCEPT all of them at glBeginQuery - the extension is core
|
||||
// since 4.6 and 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.
|
||||
// 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_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
|
||||
case GL_FRAGMENT_SHADER_INVOCATIONS:
|
||||
@@ -86,6 +108,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5078,9 +5078,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The half of the GetTexImage/GetTextureImage error set (GL 4.6 core 8.11) that depends on the
|
||||
// resolved texture object rather than on how it was named. Shared because the by-name entry
|
||||
// point does not route through GetTexImage_State and so used to enforce none of it.
|
||||
// A cube map's six faces are six independent images, and both readback spellings name one of
|
||||
// them: glGetTexImage through the TARGET token, glGetTextureSubImage through zoffset. Both then
|
||||
// have to tell the size checks below that ONE image is coming back, not six.
|
||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||
return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
}
|
||||
|
||||
// `imagesQueried` is how many of the texture's upload-target images the query hands back, and
|
||||
// exists for the destination-size check at the bottom. Zero means "all of them", which is what
|
||||
// the whole-level forms return - every face of a cube map. glGetTextureSubImage naming ONE cube
|
||||
// face passes 1: sizing that request against six faces' worth would reject the only buffer a
|
||||
// single-face read has any reason to pass.
|
||||
Bool ValidateTextureImageQuery(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
|
||||
TextureInputFormat textureInputFormat, TexturePixelDataType texturePixelDataType,
|
||||
GLsizei bufSize, const void* pixels, const char* caller) {
|
||||
GLsizei bufSize, const void* pixels, const char* caller,
|
||||
SizeT imagesQueried = 0) {
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -5196,12 +5209,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tightly packed, and summed over every face because a cube map query returns all
|
||||
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
||||
// could not have fit under any packing.
|
||||
// Tightly packed, and summed over every face because a whole-level cube map query
|
||||
// returns all six - unless the caller named a single face, which is what a non-zero
|
||||
// imagesQueried says. Pack pixel-store state only ever grows this, so a request
|
||||
// rejected here could not have fit under any packing.
|
||||
const SizeT imageCount = imagesQueried != 0 ? imagesQueried : uploadTargets.size();
|
||||
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||
texturePixelDataType, texelSize) *
|
||||
uploadTargets.size();
|
||||
imageCount;
|
||||
|
||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -5274,9 +5289,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit".
|
||||
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit". That skips
|
||||
// the destination-size branch but NOT the pixel-pack-buffer one, which measures the same
|
||||
// `required` against the bound PBO's real size - so a cube FACE query has to say it returns
|
||||
// one image here too, or a PBO sized for the one face this call packs is refused as too
|
||||
// small while the copy that follows writes exactly that much into it.
|
||||
return ValidateTextureImageQuery(textureObject, level, textureInputFormat, texturePixelDataType, -1, pixels,
|
||||
"GetTexImage_State");
|
||||
"GetTexImage_State",
|
||||
IsCubeMapFaceUploadTarget(textureUploadTarget) ? 1u : 0u);
|
||||
}
|
||||
|
||||
// What this helper can and cannot answer.
|
||||
@@ -6423,6 +6443,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The half glGetTextureImage and glGetTextureSubImage share: which of the two readbacks answers,
|
||||
// for ONE named upload target. Factored out so the sub-image form can name a cube FACE - the
|
||||
// by-name spelling of the face token glGetTexImage takes - instead of re-deriving the target and
|
||||
// silently landing on the +X face the way the delegation it replaces did.
|
||||
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
@@ -6431,16 +6468,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
__func__)) {
|
||||
return;
|
||||
}
|
||||
const auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels,
|
||||
__func__);
|
||||
GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
|
||||
bufSize, pixels, __func__);
|
||||
}
|
||||
|
||||
void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels) {
|
||||
@@ -6484,9 +6513,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && zoffset == 0 &&
|
||||
width == texelSize.x() && height == texelSize.y() &&
|
||||
depth == texelSize.z();
|
||||
// On a cube map, z is the FACE axis. A cube map's level is stored per face, so its level
|
||||
// size reads z = 1 whichever face named it - but GL 4.6 core 8.11.4 addresses the six faces
|
||||
// of a cube map through zoffset/depth, exactly the six layers a face token names for
|
||||
// glGetTexImage. Without this arm the z range was measured against that 1 and only zoffset 0
|
||||
// (the +X face) was expressible; the other five were rejected as a partial read.
|
||||
//
|
||||
// Only ONE face at a time. depth > 1 would have to concatenate faces into the destination,
|
||||
// which is the same unimplemented multi-image packing the check below still refuses.
|
||||
const Bool isSingleCubeFaceRead = textureObject->GetTarget() == TextureTarget::TextureCubeMap &&
|
||||
depth == 1 && zoffset < 6;
|
||||
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && width == texelSize.x() &&
|
||||
height == texelSize.y() &&
|
||||
(isSingleCubeFaceRead || (zoffset == 0 && depth == texelSize.z()));
|
||||
if (!isFullLevelRead) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -6495,7 +6534,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
GetTextureImage(texture, level, format, type, bufSize, pixels);
|
||||
const TextureUploadTarget readUploadTarget =
|
||||
isSingleCubeFaceRead ? static_cast<TextureUploadTarget>(
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + zoffset)
|
||||
: uploadTarget;
|
||||
if (!ValidateTextureImageQuery(textureObject, level, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type), bufSize, pixels, __func__,
|
||||
isSingleCubeFaceRead ? 1u : 0u)) {
|
||||
return;
|
||||
}
|
||||
GetTextureImageForUploadTarget(textureObject, readUploadTarget, level, format, type, bufSize, pixels,
|
||||
__func__);
|
||||
}
|
||||
|
||||
// A buffer texture carries none of the sampler or level state these queries report. Reached by
|
||||
|
||||
@@ -58,6 +58,9 @@ 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
|
||||
@@ -88,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
|
||||
@@ -97,15 +101,21 @@ 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
|
||||
@@ -115,6 +125,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -280,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()
|
||||
|
||||
@@ -345,7 +356,37 @@ 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_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
"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)
|
||||
@@ -412,6 +453,23 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
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
|
||||
@@ -508,3 +566,67 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
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}"
|
||||
)
|
||||
|
||||
@@ -225,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,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
|
||||
@@ -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,375 @@
|
||||
// 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);
|
||||
ConfigureVertexArray(m_vao);
|
||||
}
|
||||
|
||||
void ConfigureVertexArray(GLuint vao) {
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
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(GLuint vao = 0) {
|
||||
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(vao != 0 ? vao : 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";
|
||||
}
|
||||
|
||||
// Respecifying a frontend buffer preserves its VAO attachments even when the
|
||||
// backend replaces the adopted store's GL name. Keep every attribute binding
|
||||
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
|
||||
TEST_F(LargeArenaAdoptionScenario, RespecifiedVertexArenaKeepsVaoBindings) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
DrawQuad();
|
||||
ASSERT_GT(CenterPixel()[0], 200);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
GLuint otherVao = 0;
|
||||
glGenVertexArrays(1, &otherVao);
|
||||
ConfigureVertexArray(otherVao);
|
||||
DrawQuad(otherVao);
|
||||
EXPECT_GT(CenterPixel()[0], 200);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
constexpr std::array<GLsizeiptr, 3> sizes = {
|
||||
kArenaBytes, kArenaBytes + 4096, kArenaBytes - 4096,
|
||||
};
|
||||
constexpr std::array<std::array<float, 3>, 3> colors = {{
|
||||
{0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, {1.f, 0.f, 0.f},
|
||||
}};
|
||||
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||
SCOPED_TRACE(sizes[i]);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||
UploadQuad(colors[i][0], colors[i][1], colors[i][2]);
|
||||
// The unbound VAO can retain the deleted store; the current VAO's
|
||||
// attachments can be cleared by deletion. Both must be repaired.
|
||||
for (GLuint vao : {m_vao, otherVao}) {
|
||||
SCOPED_TRACE(vao);
|
||||
DrawQuad(vao);
|
||||
const auto px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
for (std::size_t channel = 0; channel < 3; ++channel) {
|
||||
if (colors[i][channel] != 0.f) {
|
||||
EXPECT_GT(px[channel], 200) << "VAO did not fetch the replacement vertex store";
|
||||
} else {
|
||||
EXPECT_LT(px[channel], 50) << "VAO still fetched the previous vertex store";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
glDeleteVertexArrays(1, &otherVao);
|
||||
}
|
||||
|
||||
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
auto vertices = QuadVertices(1.f, 0.f, 0.f);
|
||||
const auto green = QuadVertices(0.f, 1.f, 0.f);
|
||||
vertices.insert(vertices.end(), green.begin(), green.end());
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
|
||||
GLuint indices = 0;
|
||||
glGenBuffers(1, &indices);
|
||||
glBindVertexArray(m_vao);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
// Redefine through COPY_WRITE_BUFFER so the element binding slot never
|
||||
// changes. The small final store also exercises returning to shadow storage.
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, indices);
|
||||
constexpr std::array<GLsizeiptr, 4> sizes = {
|
||||
kArenaBytes, kArenaBytes, kArenaBytes + 4096, 4096,
|
||||
};
|
||||
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||
SCOPED_TRACE(sizes[i]);
|
||||
const GLuint first = (i % 2) == 0 ? 0u : 6u;
|
||||
const std::array<GLuint, 6> elements = {
|
||||
first, first + 1, first + 2, first + 3, first + 4, first + 5,
|
||||
};
|
||||
glBufferData(GL_COPY_WRITE_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, sizeof(elements), elements.data());
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
|
||||
const auto px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[first == 0 ? 0 : 1], 200) << "VAO did not fetch the replacement index store";
|
||||
EXPECT_LT(px[first == 0 ? 1 : 0], 50) << "VAO still fetched the previous index store";
|
||||
}
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
glDeleteBuffers(1, &indices);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -32,14 +32,20 @@
|
||||
// too, so the per-slice branch that exists for exactly this case was unreachable and every
|
||||
// slice above z = 0 came back VK_NULL_HANDLE.
|
||||
//
|
||||
// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
|
||||
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
|
||||
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
||||
// The first seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array
|
||||
// with its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and
|
||||
// 1D ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
||||
// not merely survival: what a layer receives is a function of its own index, so an attachment that
|
||||
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
|
||||
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
|
||||
// never landed here" reads differently from "the wrong layer landed here".
|
||||
//
|
||||
// Case (8) is the same collapse one step downstream, and case (6) is what found it: the READBACK
|
||||
// of a cube map ignored the face it was asked for and answered +X for all six. Every case here
|
||||
// that reads a layered target back depends on the readback addressing the layer it names, so it
|
||||
// belongs beside them - and case (6) had to be written around it, which is the strongest argument
|
||||
// there is that it was never pinned.
|
||||
//
|
||||
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
|
||||
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
|
||||
@@ -96,6 +102,10 @@ namespace MGITest {
|
||||
// mismatch means a real miss rather than rounding.
|
||||
constexpr Rgba8 kClearColor{17, 68, 187, 255};
|
||||
|
||||
// The six cube faces in the order GL numbers them, which is also the order Vulkan keeps
|
||||
// them in as array layers (GL 4.6 core 8.5.3 / VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT).
|
||||
const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
||||
|
||||
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
|
||||
// says which layer it came from) and b names the PASS (so "the second draw was not
|
||||
// rejected" is distinguishable from "the first draw never happened").
|
||||
@@ -357,6 +367,27 @@ void main()
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A cube map whose six faces are UPLOADED with their own colours - the same
|
||||
// ExpectedColor(face, 0) the painted cube of case (8) ends up holding, so both can be
|
||||
// checked with one expectation. Uploaded rather than rendered means the CPU shadow and
|
||||
// the image agree, which is the premise the BY-NAME readback needs; see case (8).
|
||||
GLuint MakeFaceColoredCubeMap() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const std::vector<Rgba8> seed(static_cast<std::size_t>(kExtent) * kExtent,
|
||||
ExpectedColor(face, 0));
|
||||
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
|
||||
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
|
||||
// gives 1 however many layers there are.
|
||||
@@ -487,6 +518,34 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// Every texel of one cube FACE is that face's own colour. When it is not, the message
|
||||
// says whose colour answered instead - which is the whole point here: a readback that
|
||||
// ignores the face token does not return garbage, it returns another face's perfectly
|
||||
// plausible texels, and "+X's colour came back for -Y" is the sentence that names the
|
||||
// defect. `what` is the spelling under test, since three of them read the same faces.
|
||||
void ExpectFaceColor(const std::vector<Rgba8>& texels, int face, const char* what) {
|
||||
const Rgba8 expected = ExpectedColor(face, 0);
|
||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
||||
const Rgba8 actual = texels[i];
|
||||
if (actual == expected) continue;
|
||||
std::string blame;
|
||||
if (actual.r == kPoison && actual.g == kPoison) {
|
||||
blame = " - the poison, so nothing was ever written to this face";
|
||||
} else {
|
||||
for (int other = 0; other < 6; ++other) {
|
||||
if (other != face && actual == ExpectedColor(other, 0)) {
|
||||
blame = std::string(" - which is face ") + kFaceNames[other] + "'s colour";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ADD_FAILURE() << what << ": face " << kFaceNames[face] << " texel " << i << " is "
|
||||
<< Describe(actual) << ", expected " << Describe(expected) << blame;
|
||||
// One message per face is enough to say what happened.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
@@ -883,13 +942,16 @@ void main()
|
||||
|
||||
// Every face, read back through an FBO that names THAT face.
|
||||
//
|
||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
|
||||
// where only +X had been cleared, that spelling returned the cleared colour for all
|
||||
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
|
||||
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
|
||||
// else, and the pending clear is long gone by now (materialised and popped above), so
|
||||
// this readback cannot alter what it is measuring.
|
||||
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): when this case was written
|
||||
// that spelling could not see per-face state on DirectVulkan at all - measured against
|
||||
// a tree where only +X had been cleared it returned the cleared colour for all six
|
||||
// faces - so a case built on it would have been unfalsifiable. That is a readback
|
||||
// defect rather than an attachment one, and case (8) below is where it is pinned and
|
||||
// fixed; this case keeps the independent spelling deliberately, because it must go on
|
||||
// measuring the CLEAR whatever the readback does. glFramebufferTexture2D +
|
||||
// glReadPixels names one face and nothing else, and the pending clear is long gone by
|
||||
// now (materialised and popped above), so this readback cannot alter what it is
|
||||
// measuring.
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const GLuint faceFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
||||
@@ -958,5 +1020,114 @@ void main()
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (8) THE CUBE FACE TOKEN A READBACK IS GIVEN, AND WHETHER IT HONOURS IT.
|
||||
//
|
||||
// Case (6) above had to route around glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face)
|
||||
// entirely: measured against a tree where only the +X face had been cleared, that spelling
|
||||
// returned +X's colour for all six face tokens. This case is that observation turned into
|
||||
// an assertion, and it is about the READBACK, not the attachment.
|
||||
//
|
||||
// THE DEFECT. DirectVulkan's GetTextureImage derived its copy geometry from the IMAGE's
|
||||
// target alone. A plain GL_TEXTURE_CUBE_MAP is not one of the array targets, so the layer
|
||||
// count collapsed to one - correct, one face IS one layer - but nothing ever turned the
|
||||
// face the TARGET TOKEN named into the copy's baseArrayLayer, which stayed 0. All six face
|
||||
// tokens therefore read array layer 0 and answered +X: five of a cube map's six faces were
|
||||
// unreadable through the entry point GL provides for reading them. Nothing announces it -
|
||||
// the call succeeds, raises no error, and hands back entirely plausible texels from the
|
||||
// wrong face. The conversion it was missing already existed twice over, as the clear and
|
||||
// render-pass managers' ResolveAttachmentBaseArrayLayer.
|
||||
//
|
||||
// glGetTextureSubImage is the same question asked by name: GL 4.6 core 8.11.4 addresses a
|
||||
// cube map's faces through zoffset. That spelling was not merely reading the wrong face,
|
||||
// it could not read ANY face - measured pre-fix, all six returned INVALID_OPERATION on
|
||||
// both backends. Two independent reasons, and it took both to make even zoffset 0 fail:
|
||||
// the z range was measured against the level's z, which is one face's 1, so five of the
|
||||
// six looked like a partial read; and the destination-size check summed all six faces, so
|
||||
// the one face's worth of buffer a single-face read has any reason to pass was rejected as
|
||||
// too small.
|
||||
//
|
||||
// Each face is painted its OWN colour, so a collapse onto layer 0 does not merely read
|
||||
// "wrong": the failure names the face that answered. The cube is poisoned first and then
|
||||
// painted through the GPU, so an answer served from the stale CPU shadow is also called out
|
||||
// by name rather than passing. And the per-face FBO + glReadPixels read is the control: it
|
||||
// names one face and nothing else, so if IT disagrees the defect is in how the faces were
|
||||
// written and this case is measuring the wrong thing.
|
||||
//
|
||||
// DirectGLES attaches the named face to a scratch FBO and reads that, so it answers the
|
||||
// face token correctly throughout - a red there means this case is wrong. Its by-name
|
||||
// readback is a different matter and gets a texture of its own; see the third block.
|
||||
TEST_F(LayeredAttachmentShapeScenario, CubeMapFaceReadbackAnswersTheFaceItWasAskedFor) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint cube = MakePoisonedCubeMap();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
|
||||
|
||||
// Paint every face its own colour through an FBO that names that one face. A clear
|
||||
// rather than a draw, so nothing here depends on a shader stage being present.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
GLuint faceFbos[6] = {};
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
faceFbos[face] = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
|
||||
const Rgba8 want = ExpectedColor(face, 0);
|
||||
glClearColor(want.r / 255.0f, want.g / 255.0f, want.b / 255.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "painting the six faces errored";
|
||||
|
||||
// The control. If this is red, the faces do not hold six different values and the two
|
||||
// readbacks below are being measured against a premise that is not true.
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the control read of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "control: per-face FBO + glReadPixels");
|
||||
}
|
||||
|
||||
// The subject: the face TOKEN.
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glGetTexImage(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "glGetTexImage(GL_TEXTURE_CUBE_MAP_<face>)");
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
|
||||
// The same question by name, where zoffset is the face.
|
||||
//
|
||||
// On a cube map UPLOADED face by face rather than the painted one above, because the
|
||||
// by-name readback has no backend entry outside DirectVulkan and answers from the CPU
|
||||
// shadow there - a separate, pre-existing gap that has nothing to do with which face
|
||||
// gets read. Asking it about GPU-painted content would make this red on DirectGLES for
|
||||
// a reason the case is not about; asking it about uploaded content leaves exactly one
|
||||
// thing either backend can get wrong, which is the face. DirectVulkan still answers
|
||||
// this one out of the image, so the layer collapse is just as visible here.
|
||||
const GLuint uploaded = MakeFaceColoredCubeMap();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "uploading the six faces failed";
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glGetTextureSubImage(uploaded, 0, 0, 0, face, kExtent, kExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
static_cast<GLsizei>(texels.size() * sizeof(Rgba8)), texels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTextureSubImage of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "glGetTextureSubImage(zoffset = face)");
|
||||
}
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // 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,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,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,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,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
|
||||
@@ -119,6 +119,73 @@ void main() {
|
||||
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
|
||||
@@ -369,6 +436,65 @@ void main() {
|
||||
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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -525,7 +525,7 @@ 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 in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// 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
|
||||
@@ -551,10 +551,10 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
// 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_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
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_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
"MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
}
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
@@ -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);
|
||||
@@ -126,6 +139,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// 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) {
|
||||
@@ -133,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);
|
||||
@@ -144,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) {
|
||||
@@ -160,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};
|
||||
@@ -197,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);
|
||||
}
|
||||
@@ -253,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.");
|
||||
@@ -269,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;
|
||||
@@ -305,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);
|
||||
}
|
||||
@@ -340,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;
|
||||
}
|
||||
@@ -358,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();
|
||||
|
||||
|
||||
@@ -710,8 +710,15 @@ namespace MobileGL::MG_State {
|
||||
// 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::Vertex}) {
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
|
||||
ShaderStage::Vertex}) {
|
||||
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
|
||||
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
|
||||
if (!captureProgram) continue;
|
||||
|
||||
@@ -517,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
|
||||
@@ -855,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.
|
||||
@@ -896,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);
|
||||
@@ -1856,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);
|
||||
@@ -1869,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) {
|
||||
@@ -1881,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;
|
||||
@@ -1910,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
|
||||
|
||||
@@ -554,7 +554,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
// asked for is the safer of the two readings.
|
||||
if (task->in.requestedXfbVaryings.empty()) {
|
||||
for (const ShaderStage captureStage:
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::TessControl,
|
||||
ShaderStage::Vertex}) {
|
||||
Bool stagePresent = false;
|
||||
for (const auto& shader : m_shaders) {
|
||||
if (!shader || shader->GetShaderStage() != captureStage) continue;
|
||||
|
||||
@@ -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());
|
||||
@@ -1429,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 ----
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,12 +67,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
|
||||
Bool nativeFloat64);
|
||||
Bool nativeFloat64, Bool demoteTessellationPointSize,
|
||||
Bool demoteGeometryPointSize);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
// ProgramLinkTask::DeferLog.
|
||||
void DeferLog(String line);
|
||||
// ProgramLinkTask::DeferLog, and the same severity rule: DEBUG is compiled out of
|
||||
// every shipped build, so a line that has to survive one names its own level.
|
||||
void DeferLog(String line, Int level = MOBILEGL_LOG_LEVEL_DEBUG);
|
||||
|
||||
SharedPtr<ProgramLinkTask> m_phaseA;
|
||||
};
|
||||
|
||||
@@ -384,12 +384,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// the log, for every failing shader. The info log is what names the offending
|
||||
// line; the source is recoverable from the application.
|
||||
const SizeT firstLineEnd = artifacts.infoLog.find('\n');
|
||||
diagnostics.logLines.push_back(std::format(
|
||||
"ShaderCompileTask: shader {} (stage {}) failed to compile; compileStatus = false. "
|
||||
"Preprocessed source: {} bytes. First log line: {}",
|
||||
externalIndex, static_cast<Int>(stage), shared.preprocessedSource.length(),
|
||||
artifacts.infoLog.substr(0, firstLineEnd == String::npos ? artifacts.infoLog.length()
|
||||
: firstLineEnd)));
|
||||
diagnostics.logLines.push_back(
|
||||
{MOBILEGL_LOG_LEVEL_DEBUG,
|
||||
std::format(
|
||||
"ShaderCompileTask: shader {} (stage {}) failed to compile; compileStatus = false. "
|
||||
"Preprocessed source: {} bytes. First log line: {}",
|
||||
externalIndex, static_cast<Int>(stage), shared.preprocessedSource.length(),
|
||||
artifacts.infoLog.substr(0, firstLineEnd == String::npos
|
||||
? artifacts.infoLog.length()
|
||||
: firstLineEnd))});
|
||||
if (shouldPopulateCache) {
|
||||
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
||||
fresh->infoLog = artifacts.infoLog;
|
||||
|
||||
@@ -97,27 +97,31 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_storageOwner->HasFixedSampleLocations();
|
||||
}
|
||||
|
||||
Uint TextureObjectView::ViewLayerIndex(TextureUploadTarget viewTarget) const {
|
||||
if (GetTarget() != TextureTarget::TextureCubeMap) {
|
||||
// One target, one layer: the view's origin is the whole answer.
|
||||
return m_viewMinLayer;
|
||||
}
|
||||
for (Uint i = 0; i < static_cast<Uint>(m_uploadTargets.size()); ++i) {
|
||||
if (m_uploadTargets[i] == viewTarget) return m_viewMinLayer + i;
|
||||
}
|
||||
return m_viewMinLayer;
|
||||
}
|
||||
|
||||
TextureUploadTarget TextureObjectView::ToOwnerUploadTarget(TextureUploadTarget viewTarget) const {
|
||||
const auto& ownerTargets = m_storageOwner->GetUploadTargets();
|
||||
MOBILEGL_ASSERT(!ownerTargets.empty(), "TextureObjectView: storage owner has no upload target");
|
||||
if (ownerTargets.size() == 1) {
|
||||
// The owner keeps every layer in one blob, so there is nothing to choose.
|
||||
// The owner keeps every layer in one blob, so there is nothing to choose HERE - which
|
||||
// is exactly why a cube-map view over such an owner has to have its face carried by
|
||||
// LayerByteOffset instead. See ViewLayerIndex.
|
||||
return ownerTargets[0];
|
||||
}
|
||||
// The owner is a cube map: six independent blobs, one per face, and the view's layer
|
||||
// index selects among them. A cube-map view of a cube map maps face to face; any other
|
||||
// view target addresses layers, which for a cube-map owner ARE its faces.
|
||||
// The owner is a cube map: six independent blobs, one per face, and the layer this view
|
||||
// target names selects among them. A cube-map view of a cube map maps face to face; any
|
||||
// other view target addresses layers, which for a cube-map owner ARE its faces.
|
||||
const Uint faceCount = static_cast<Uint>(ownerTargets.size());
|
||||
Uint face = m_viewMinLayer;
|
||||
if (GetTarget() == TextureTarget::TextureCubeMap) {
|
||||
for (Uint i = 0; i < m_uploadTargets.size(); ++i) {
|
||||
if (m_uploadTargets[i] == viewTarget) {
|
||||
face = m_viewMinLayer + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ownerTargets[std::min(face, faceCount - 1)];
|
||||
return ownerTargets[std::min(ViewLayerIndex(viewTarget), faceCount - 1)];
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectView::ToViewLevelSize(const IntVec3& ownerLevelSize) const {
|
||||
@@ -151,7 +155,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
SizeT TextureObjectView::LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const {
|
||||
if (m_viewMinLayer == 0 || m_ownerMipmap == nullptr) return 0;
|
||||
if (m_ownerMipmap == nullptr) return 0;
|
||||
// The FACE is part of this, not just the view's origin: a cube-map view over a layered
|
||||
// owner (a 2D array or a cube-map ARRAY) has only one blob to address, so the face its
|
||||
// target token names lives here or nowhere. It used to live nowhere, and all six face
|
||||
// tokens read the view's first layer-face - silently, with texels from a real layer.
|
||||
const Uint layerIndex = ViewLayerIndex(viewTarget);
|
||||
if (layerIndex == 0) return 0;
|
||||
const LayerAxis ownerAxis = LayerAxisOf(m_storageOwner->GetTarget());
|
||||
if (ownerAxis == LayerAxis::None) {
|
||||
// A cube-map owner keeps each face in its OWN blob, and ToOwnerUploadTarget already
|
||||
@@ -173,23 +183,26 @@ namespace MobileGL::MG_State::GLState {
|
||||
? static_cast<SizeT>(std::max(ownerSize.x(), 0))
|
||||
: static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(ownerSize.y(), 0));
|
||||
const SizeT offset = static_cast<SizeT>(m_viewMinLayer) * layerTexels * bytesPerTexel;
|
||||
const SizeT offset = static_cast<SizeT>(layerIndex) * layerTexels * bytesPerTexel;
|
||||
return offset < ownerBytes ? offset : 0;
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectView::ToOwnerRegionOffset(const IntVec3& viewOffset) const {
|
||||
if (m_viewMinLayer == 0) return viewOffset;
|
||||
IntVec3 TextureObjectView::ToOwnerRegionOffset(TextureUploadTarget viewTarget, const IntVec3& viewOffset) const {
|
||||
const Uint layerIndex = ViewLayerIndex(viewTarget);
|
||||
if (layerIndex == 0) return viewOffset;
|
||||
IntVec3 offset = viewOffset;
|
||||
// The dirty region is recorded in the OWNER's blob coordinates - that is the space its
|
||||
// upload path walks - so the view's layer origin has to be added here even though
|
||||
// upload path walks - so the layer this view target names has to be added here even though
|
||||
// MapMipmapData hands back an already-shifted POINTER. The two are not double-counting:
|
||||
// one moves the bytes, the other tells the owner which of its layers moved.
|
||||
// one moves the bytes, the other tells the owner which of its layers moved. They must agree
|
||||
// on the layer, which is why both ask ViewLayerIndex rather than reading m_viewMinLayer -
|
||||
// on a cube-map view the face is half the answer.
|
||||
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
|
||||
case LayerAxis::Y:
|
||||
offset.y() += static_cast<Int>(m_viewMinLayer);
|
||||
offset.y() += static_cast<Int>(layerIndex);
|
||||
break;
|
||||
case LayerAxis::Z:
|
||||
offset.z() += static_cast<Int>(m_viewMinLayer);
|
||||
offset.z() += static_cast<Int>(layerIndex);
|
||||
break;
|
||||
case LayerAxis::None:
|
||||
break;
|
||||
@@ -300,7 +313,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
IntVec3 size) {
|
||||
if (m_ownerMipmap == nullptr) return;
|
||||
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
||||
ToOwnerRegionOffset(offset), size);
|
||||
ToOwnerRegionOffset(uploadTarget, offset), size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObjectView::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
|
||||
@@ -103,6 +103,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
// target - arrays and cube-map arrays included - keeps all its layers in one blob, so
|
||||
// the mapping is "the owner's only target" unless one of the two sides is a cube map.
|
||||
TextureUploadTarget ToOwnerUploadTarget(TextureUploadTarget viewTarget) const;
|
||||
// WHICH of the owner's layers a given view-side upload target names, in the owner's layer
|
||||
// numbering. For every view target but a cube map that is just this view's layer origin -
|
||||
// one target, one layer. A GL_TEXTURE_CUBE_MAP view addresses SIX of the owner's layers at
|
||||
// once (GL 4.6 core 8.18), so the face its target token names is an index on top of that
|
||||
// origin, and this is the only place that can express it when the owner keeps every layer
|
||||
// in one blob: ToOwnerUploadTarget has a single blob to choose from there, so the face
|
||||
// would otherwise vanish and all six tokens would read the view's first layer.
|
||||
//
|
||||
// Every place that turns this view into owner-side bytes goes through here - the blob
|
||||
// choice, the byte offset, and the dirty region - so the three cannot disagree about which
|
||||
// layer a face is.
|
||||
Uint ViewLayerIndex(TextureUploadTarget viewTarget) const;
|
||||
Uint ToOwnerLevel(Uint viewLevel) const { return m_viewMinLevel + viewLevel; }
|
||||
// The owner's level extent rewritten into this view's shape: the owner's layer axis is
|
||||
// collapsed to one slice and the view's own layer count is imposed on the view's layer
|
||||
@@ -114,8 +126,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// and a single row for a 1D array; a cube-map owner returns 0 because its faces are
|
||||
// separate blobs that ToOwnerUploadTarget already selects between.
|
||||
SizeT LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const;
|
||||
// A dirty-region origin moved from the view's layer space into the owner's.
|
||||
IntVec3 ToOwnerRegionOffset(const IntVec3& viewOffset) const;
|
||||
// A dirty-region origin moved from the view's layer space into the owner's. Takes the view
|
||||
// target for the same reason LayerByteOffset does: on a cube-map view the target names the
|
||||
// face, and the region has to name the same owner layer the bytes were written to.
|
||||
IntVec3 ToOwnerRegionOffset(TextureUploadTarget viewTarget, const IntVec3& viewOffset) const;
|
||||
|
||||
SharedPtr<ITextureObject> m_storageOwner;
|
||||
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in
|
||||
|
||||
@@ -99,9 +99,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY
|
||||
// texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the
|
||||
// setters that feed it all early-out when the value is unchanged, so the redundant
|
||||
// glTexParameteri calls applications issue every frame do not churn it. Kept separate
|
||||
// from the bind generation because the sampled texture SET is unaffected by these, and
|
||||
// the Vulkan backend's set memo keys on that one.
|
||||
// glTexParameteri calls applications issue every frame do not churn it.
|
||||
//
|
||||
// Kept separate from the bind generation because the two answer different questions, NOT
|
||||
// because the sampled texture SET is immune to this one - it is not, and the claim that
|
||||
// it was is what this comment used to say. DirectVulkan leaves an incomplete texture out
|
||||
// of the set entirely and substitutes a fallback, so completeness decides membership, and
|
||||
// its sampled-set memo carries THIS generation alongside the bind one. Any memo of a
|
||||
// resolved per-unit binding - or of which textures a draw samples at all - needs both.
|
||||
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
|
||||
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::PointSizeExtensionName;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestPointSizeExtension;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
||||
@@ -1289,6 +1291,80 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
||||
}
|
||||
|
||||
// --- tessellation / geometry gl_PointSize directive ---------------------------------------------
|
||||
//
|
||||
// ESSL 320 makes the tessellation and geometry STAGES core and still leaves gl_PointSize out of
|
||||
// their gl_PerVertex entirely - it is only there under EXT/OES_tessellation_point_size resp.
|
||||
// EXT/OES_geometry_point_size. SPIRV-Cross only ever sees a SPIR-V BuiltIn PointSize decoration
|
||||
// and prints the identifier bare, so without this directive the stage fails to compile with
|
||||
// "`gl_PointSize' undeclared", which takes the WHOLE program to program 0: the draw renders
|
||||
// nothing and glBeginTransformFeedback on that program is rejected outright, so a capture of
|
||||
// anything at all off it silently comes back empty. That is the shape of the 108 conformance
|
||||
// bodies (36 per API tree) in tessellation_control_to_tessellation_evaluation.gl_MaxPatch-
|
||||
// Vertices_Position_PointSize whose point_mode half puts gl_PointSize in the patch.
|
||||
|
||||
TEST(PointSizeExtensionNameTest, NamesBothSpellingsOfBothExtensions) {
|
||||
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
||||
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, true), "GL_EXT_tessellation_point_size");
|
||||
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, true), "GL_OES_tessellation_point_size");
|
||||
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionEXT, false), "GL_EXT_geometry_point_size");
|
||||
EXPECT_STREQ(PointSizeExtensionName(Tier::ExtensionOES, false), "GL_OES_geometry_point_size");
|
||||
}
|
||||
|
||||
// The two extensions are separate and neither implies the other, so the tessellation answer must
|
||||
// never be handed to a geometry stage or the other way round - an `#extension` naming a string
|
||||
// the driver does not advertise is itself a compile error on a strict compiler.
|
||||
TEST(PointSizeExtensionNameTest, NoTierMeansNoDirective) {
|
||||
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
||||
EXPECT_EQ(PointSizeExtensionName(Tier::None, true), nullptr);
|
||||
EXPECT_EQ(PointSizeExtensionName(Tier::None, false), nullptr);
|
||||
}
|
||||
|
||||
TEST(RequestPointSizeExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(triangles, point_mode, cw, equal_spacing) in;
|
||||
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
|
||||
)";
|
||||
const String out = RequestPointSizeExtension(source, "GL_EXT_tessellation_point_size");
|
||||
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_EXT_tessellation_point_size : require\n")) << out;
|
||||
}
|
||||
|
||||
// The nullptr contract, and the reason it exists: a driver that advertises neither spelling gets
|
||||
// NOTHING added rather than a directive it would reject on top of the error it already has.
|
||||
TEST(RequestPointSizeExtensionTest, ANullNameMeansNotEmitted) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(triangles, point_mode, cw, equal_spacing) in;
|
||||
void main() { gl_Position = vec4(0.0); gl_PointSize = 5.0; }
|
||||
)";
|
||||
EXPECT_EQ(RequestPointSizeExtension(source, nullptr), source);
|
||||
}
|
||||
|
||||
TEST(RequestPointSizeExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
||||
const String source = R"(#version 320 es
|
||||
#extension GL_OES_tessellation_point_size : require
|
||||
layout(triangles, point_mode, cw, equal_spacing) in;
|
||||
void main() { gl_PointSize = 5.0; }
|
||||
)";
|
||||
const String out = RequestPointSizeExtension(source, "GL_OES_tessellation_point_size");
|
||||
EXPECT_EQ(out, source);
|
||||
EXPECT_EQ(CountOf(out, "GL_OES_tessellation_point_size"), 1u) << out;
|
||||
}
|
||||
|
||||
// Shares its insertion point with the viewport-array and image-format directives, so a stage
|
||||
// needing more than one must end up with all of them and with #version still first.
|
||||
TEST(RequestPointSizeExtensionTest, CoexistsWithTheOtherHeaderDirectives) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() { gl_ViewportIndex = 1; gl_PointSize = 2.0; EmitVertex(); }
|
||||
)";
|
||||
const String out = RequestPointSizeExtension(RequestViewportArrayExtension(source, true),
|
||||
"GL_EXT_geometry_point_size");
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_geometry_point_size : require\n")) << out;
|
||||
}
|
||||
|
||||
// --- pass-through tessellation control stage --------------------------------------------------
|
||||
//
|
||||
// Desktop GL makes the tessellation control stage optional and takes the levels from
|
||||
@@ -1303,6 +1379,20 @@ namespace {
|
||||
const FloatVec2 kDefaultInner(1.0f, 1.0f);
|
||||
} // namespace
|
||||
|
||||
// The synthesized stage mirrors its neighbours' gl_PerVertex, so it can be the thing that
|
||||
// declares gl_PointSize - and in ESSL a redeclaration is exactly as illegal as a reference
|
||||
// without the extension. The directive has to survive being applied to its output.
|
||||
TEST(RequestPointSizeExtensionTest, CoversAMirroredPassthroughControlStage) {
|
||||
const String out = RequestPointSizeExtension(
|
||||
BuildPassthroughTessControlEssl(320, 4, " highp vec4 gl_Position; highp float gl_PointSize; ",
|
||||
" highp vec4 gl_Position; highp float gl_PointSize; ", kDefaultOuter,
|
||||
kDefaultInner),
|
||||
"GL_EXT_tessellation_point_size");
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_point_size : require\n")) << out;
|
||||
EXPECT_TRUE(Contains(out, "float gl_PointSize")) << out;
|
||||
}
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -14,9 +15,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||
|
||||
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
|
||||
// a GLESFunctionsTable populated with captureless lambdas backed by the file-scope
|
||||
@@ -25,6 +28,13 @@
|
||||
// ANGLE-style baseInstance-leaking driver, or a failing one.
|
||||
namespace {
|
||||
struct FakeDriverState {
|
||||
// What each of the located-interface-block probe's draws reads back, in the order the
|
||||
// probe makes them: unlocated control, located subject, located vertex-to-fragment
|
||||
// control. Empty means "conforming driver" - every read returns the payload - which is
|
||||
// what keeps this probe invisible to every other test in this file.
|
||||
std::vector<bool> ioBlockPayloadArrives;
|
||||
std::size_t ioBlockReads = 0;
|
||||
std::size_t ioBlockDraws = 0;
|
||||
// Behavior knobs, configured per test before running the probe.
|
||||
GLint maxVertexSsboBlocks = 4;
|
||||
GLint glesMajorVersion = 3;
|
||||
@@ -433,6 +443,52 @@ namespace {
|
||||
};
|
||||
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
||||
funcs.glBindRenderbuffer = [](GLenum, GLuint) {};
|
||||
// ---- what the located-interface-block probe draws with -------------------------
|
||||
// Enough of a rasterizer for ProbeLocatedIoBlocksLosePayload to reach a verdict: it
|
||||
// builds three programs, draws each to a 1x1 target and reads the pixel back, and the
|
||||
// fake decides what each read returns. Default behaviour is a CONFORMING driver, so
|
||||
// every test that predates this one sees the probe reach "not affected" and no
|
||||
// capability it asserts on moves.
|
||||
funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
|
||||
funcs.glViewport = [](GLint, GLint, GLsizei, GLsizei) {};
|
||||
funcs.glClearColor = [](GLfloat, GLfloat, GLfloat, GLfloat) {};
|
||||
funcs.glClear = [](GLbitfield) {};
|
||||
funcs.glPixelStorei = [](GLenum, GLint) {};
|
||||
funcs.glColorMask = [](GLboolean, GLboolean, GLboolean, GLboolean) {};
|
||||
funcs.glIsEnabled = [](GLenum) -> GLboolean { return GL_FALSE; };
|
||||
funcs.glGetBooleanv = [](GLenum, GLboolean* data) {
|
||||
if (data == nullptr) return;
|
||||
for (int i = 0; i < 4; ++i) data[i] = GL_TRUE;
|
||||
};
|
||||
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) {
|
||||
if (data != nullptr) *data = 0;
|
||||
};
|
||||
funcs.glGetProgramInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
|
||||
if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0';
|
||||
if (length != nullptr) *length = 0;
|
||||
};
|
||||
funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
|
||||
if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0';
|
||||
if (length != nullptr) *length = 0;
|
||||
};
|
||||
funcs.glDrawArrays = [](GLenum, GLint, GLsizei) { ++g_fake.ioBlockDraws; };
|
||||
// One entry of ioBlockPayloadArrives is consumed per draw, in the order the probe makes
|
||||
// them: the unlocated CONTROL, then the located SUBJECT, then the located
|
||||
// vertex-to-fragment second control. Past the end of the list the driver is conforming.
|
||||
funcs.glReadPixels = [](GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void* pixels) {
|
||||
auto* out = static_cast<unsigned char*>(pixels);
|
||||
if (out == nullptr) return;
|
||||
const std::size_t index = g_fake.ioBlockReads++;
|
||||
const bool arrives = index < g_fake.ioBlockPayloadArrives.size()
|
||||
? g_fake.ioBlockPayloadArrives[index]
|
||||
: true;
|
||||
// 0.25 and 0.5 as the probe's vertex stage wrote them; zeroes are what a stage that
|
||||
// received nothing reads.
|
||||
out[0] = arrives ? 0x40 : 0x00;
|
||||
out[1] = arrives ? 0x80 : 0x00;
|
||||
out[2] = 0x00;
|
||||
out[3] = 0xff;
|
||||
};
|
||||
funcs.glRenderbufferStorage = [](GLenum, GLenum, GLsizei, GLsizei) {};
|
||||
funcs.glFramebufferRenderbuffer = [](GLenum, GLenum, GLenum, GLuint) {};
|
||||
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||
@@ -1347,3 +1403,151 @@ TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
||||
}
|
||||
|
||||
// ===================== LOCATED INTER-STAGE INTERFACE BLOCKS =====================
|
||||
//
|
||||
// The capability that decides whether DirectGLES strips the layout(location) qualifier off a
|
||||
// tessellation/geometry program's interface blocks, and the environment override that forces
|
||||
// it either way.
|
||||
//
|
||||
// THE MAPPING IS INVERTED ON PURPOSE and that is exactly why it is pinned here: the variable
|
||||
// is named for the EMULATION ("emit them unlocated"), the capability is named for the DRIVER
|
||||
// ("located blocks work"), so forcing the emulation ON must set the capability to FALSE. A
|
||||
// one-line swap of those two arms would leave every other test in the tree green - the unit
|
||||
// tests drive the pass directly, and the integration lane runs on llvmpipe, which carries a
|
||||
// located block correctly either way - while silently disabling the repair on the only device
|
||||
// that needs it.
|
||||
|
||||
namespace {
|
||||
void SetEnvVarForTest(const char* name, const char* value) {
|
||||
#if defined(_WIN32)
|
||||
_putenv_s(name, value);
|
||||
#else
|
||||
setenv(name, value, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UnsetEnvVarForTest(const char* name) {
|
||||
#if defined(_WIN32)
|
||||
_putenv_s(name, "");
|
||||
#else
|
||||
unsetenv(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sets MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS (or clears it), re-reads the configuration the
|
||||
// way process start would, and runs the capability fill against the fake driver.
|
||||
MobileGL::MG_External::GLESCapabilities CapabilitiesWithOverride(
|
||||
const MobileGL::MG_External::GLESFunctionsTable& funcs, const char* value) {
|
||||
if (value == nullptr) {
|
||||
UnsetEnvVarForTest("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
} else {
|
||||
SetEnvVarForTest("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS", value);
|
||||
}
|
||||
MobileGL::MG_ConfigLoader::Init();
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
EXPECT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
return caps;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(LocatedIoBlockCapability, TheOverrideMapsOntoTheCapabilityInverted) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
// ONE TEST, THREE ARMS, IN THIS ORDER, because the Auto arm consults a probe that is
|
||||
// memoized for the lifetime of the process - splitting them into three test cases would
|
||||
// make the answer depend on which one gtest happened to run first.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
|
||||
// ForceOn - "emit the blocks unlocated". The driver is NOT probed, and the capability must
|
||||
// come out FALSE. This is the assertion the inversion swap breaks.
|
||||
{
|
||||
const auto caps = CapabilitiesWithOverride(funcs, "1");
|
||||
EXPECT_FALSE(caps.SupportsLocatedInterStageIoBlocks)
|
||||
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 forces the emulation ON, which means "
|
||||
"declaring that this driver's located interface blocks do NOT work. A true here "
|
||||
"means the strip is disabled in the one configuration that exists to enable it.";
|
||||
}
|
||||
|
||||
// ForceOff - the negative control. Also unprobed, and the capability must come out TRUE so
|
||||
// the strip stays off.
|
||||
{
|
||||
const auto caps = CapabilitiesWithOverride(funcs, "0");
|
||||
EXPECT_TRUE(caps.SupportsLocatedInterStageIoBlocks)
|
||||
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=0 forces located blocks ON, i.e. the "
|
||||
"emulation off; a false here would strip on every driver regardless of the probe.";
|
||||
}
|
||||
|
||||
// Auto - the setting every real run uses. The capability is the probe's verdict, negated:
|
||||
// "the blocks lose their payload" is the same statement as "located blocks are not
|
||||
// supported". On this fake the probe finds a conforming driver, so the capability is true.
|
||||
{
|
||||
const auto caps = CapabilitiesWithOverride(funcs, nullptr);
|
||||
EXPECT_EQ(caps.SupportsLocatedInterStageIoBlocks,
|
||||
!MobileGL::MG_Util::SelfTest::LocatedIoBlocksLosePayload(funcs).detected)
|
||||
<< "with the variable unset the capability must follow the driver probe and nothing "
|
||||
"else";
|
||||
EXPECT_TRUE(caps.SupportsLocatedInterStageIoBlocks)
|
||||
<< "the fake driver carries the probe's payload, so Auto must leave the strip off";
|
||||
}
|
||||
|
||||
UnsetEnvVarForTest("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
MobileGL::MG_ConfigLoader::Init();
|
||||
}
|
||||
|
||||
// The probe's own verdict logic, driven directly rather than through the memoized accessor so
|
||||
// each shape gets its own answer. Its two controls are the whole design: without them a driver
|
||||
// that cannot run the shape at all, or one whose interface blocks are broken generally, would
|
||||
// be reported as having this very specific defect - and would have its locations stripped for
|
||||
// nothing.
|
||||
TEST(LocatedIoBlockProbe, ReportsTheDefectOnlyWhenTheUnlocatedControlCarriesThePayload) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
using MobileGL::MG_Util::SelfTest::ProbeLocatedIoBlocksLosePayload;
|
||||
|
||||
// A CONFORMING driver: every draw delivers. No finding.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.ioBlockPayloadArrives = {true, true, true};
|
||||
EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(funcs).detected);
|
||||
|
||||
// THE AFFECTED DRIVER: the unlocated control delivers, the located subject does not, and
|
||||
// the located vertex-to-fragment control does. That last one is what scopes the repair to
|
||||
// tessellation/geometry programs.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.ioBlockPayloadArrives = {true, false, true};
|
||||
{
|
||||
const auto measurement = ProbeLocatedIoBlocksLosePayload(funcs);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_FALSE(measurement.alsoAffectsVertexToFragment);
|
||||
}
|
||||
|
||||
// ...and a driver that loses the payload even without a geometry stage says so, because the
|
||||
// repair does not reach that shape and the report must not imply it does.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.ioBlockPayloadArrives = {true, false, false};
|
||||
{
|
||||
const auto measurement = ProbeLocatedIoBlocksLosePayload(funcs);
|
||||
EXPECT_TRUE(measurement.detected);
|
||||
EXPECT_TRUE(measurement.alsoAffectsVertexToFragment);
|
||||
}
|
||||
|
||||
// THE CONTROL FAILING IS NOT A FINDING. A driver that cannot carry an UNLOCATED block
|
||||
// either has something else wrong with it, and stripping locations would repair nothing
|
||||
// while changing every tessellation and geometry program on it.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.ioBlockPayloadArrives = {false, false, false};
|
||||
EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(funcs).detected);
|
||||
|
||||
// Neither is a driver the probe cannot even draw on: an inconclusive probe must leave the
|
||||
// capability exactly as it was before the probe existed.
|
||||
ResetFakeDriver();
|
||||
g_fake.glesMinorVersion = 2;
|
||||
auto crippled = MakeFakeGLESFunctions();
|
||||
crippled.glReadPixels = nullptr;
|
||||
EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(crippled).detected);
|
||||
EXPECT_EQ(g_fake.ioBlockDraws, 0u) << "an entry-point-gated probe must not draw at all";
|
||||
}
|
||||
|
||||
@@ -1568,6 +1568,15 @@ namespace {
|
||||
int respecifyCalls = 0;
|
||||
int flushCalls = 0;
|
||||
Bool provideMap = true; // false => backend declines, exercising the shadow fallback
|
||||
// Only recorded by the variant of the ops table that offers ResidentSubData: the
|
||||
// bytes a CPU write handed the backend for a GPU-ordered landing into an adopted
|
||||
// store, held back from `gpu` until a readback "retires" them.
|
||||
struct ResidentWrite {
|
||||
SizeT offset = 0;
|
||||
Vector<Uint8> bytes;
|
||||
};
|
||||
Vector<ResidentWrite> residentWrites;
|
||||
int readbackCalls = 0;
|
||||
};
|
||||
|
||||
ZeroCopyMockBackend* g_zeroCopyMock = nullptr;
|
||||
@@ -1605,6 +1614,39 @@ namespace {
|
||||
.AcquirePersistentMap = ZeroCopyMock_AcquirePersistentMap,
|
||||
};
|
||||
|
||||
// The same backend with the GPU-ordered landing ops a staging-ring backend offers: a
|
||||
// CPU write into an adopted store is queued (the mapping is NOT written through), and
|
||||
// a readback is what lands the queue before the application reads.
|
||||
void ZeroCopyMock_ResidentSubData(MG_State::GLState::BufferObject&, SizeT offset, DataPtr data) {
|
||||
if (!g_zeroCopyMock) return;
|
||||
auto& write = g_zeroCopyMock->residentWrites.emplace_back();
|
||||
write.offset = offset;
|
||||
const auto* bytes = static_cast<const Uint8*>(data.data);
|
||||
write.bytes.assign(bytes, bytes + data.size);
|
||||
}
|
||||
void ZeroCopyMock_ReadbackFromGpu(MG_State::GLState::BufferObject&) {
|
||||
if (!g_zeroCopyMock) return;
|
||||
++g_zeroCopyMock->readbackCalls;
|
||||
for (const auto& write : g_zeroCopyMock->residentWrites) {
|
||||
// Reported, not asserted: an ASSERT here would return out of the readback and
|
||||
// leave the remaining landings unapplied, which reads as a different failure.
|
||||
EXPECT_LE(write.offset + write.bytes.size(), g_zeroCopyMock->gpu.size());
|
||||
if (write.offset + write.bytes.size() > g_zeroCopyMock->gpu.size()) continue;
|
||||
Memcpy(g_zeroCopyMock->gpu.data() + write.offset, write.bytes.data(), write.bytes.size());
|
||||
}
|
||||
g_zeroCopyMock->residentWrites.clear();
|
||||
}
|
||||
|
||||
const MG_State::GLState::BufferBackendOps kResidentSubDataMockOps = {
|
||||
.Respecify = ZeroCopyMock_Respecify,
|
||||
.SubData = ZeroCopyMock_SubData,
|
||||
.ResidentSubData = ZeroCopyMock_ResidentSubData,
|
||||
.FlushMappedRange = ZeroCopyMock_Flush,
|
||||
.OnDestroy = ZeroCopyMock_OnDestroy,
|
||||
.AcquirePersistentMap = ZeroCopyMock_AcquirePersistentMap,
|
||||
.ReadbackFromGpu = ZeroCopyMock_ReadbackFromGpu,
|
||||
};
|
||||
|
||||
struct ScopedBackendOps {
|
||||
explicit ScopedBackendOps(const MG_State::GLState::BufferBackendOps* ops) {
|
||||
MG_State::GLState::SetBufferBackendOps(ops);
|
||||
@@ -2074,3 +2116,861 @@ TEST_F(BufferTest, RedefiningANonAdoptedBufferIsUnchanged) {
|
||||
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A NON-persistent write map of an ADOPTED store. glMapBuffer / glMapBufferRange hand
|
||||
// the application a staging copy regardless of where the store lives, and GL requires
|
||||
// the bytes it wrote there to be visible to every later command once glUnmapBuffer
|
||||
// returns. Residency used to come only from a coherent persistent map - which writes
|
||||
// in place and never has a staging copy - so the unmap simply skipped the copy-back
|
||||
// for a resident store. Residency now also comes from a shader storage binding
|
||||
// (EnsureGpuResidentStorage at draw time) and from large-store adoption, both of which
|
||||
// an application then re-initialises through an ordinary map/write/unmap: the
|
||||
// conformance suite re-seeds every SSBO that way before each draw, and every re-seed
|
||||
// after the first draw was dropped on the floor. These pin the landing for each map
|
||||
// shape, on the backend that writes the coherent mapping in place and on the one that
|
||||
// takes the bytes for a GPU-ordered landing, plus the shadow path as the control.
|
||||
namespace {
|
||||
constexpr SizeT kAdoptedInts = 16;
|
||||
|
||||
// A buffer of kAdoptedInts sequential ints, adopted by the mock backend exactly as an
|
||||
// SSBO binding does at draw time. The per-write counters are zeroed afterwards so a
|
||||
// test only sees the traffic of the map it makes.
|
||||
SharedPtr<MG_State::GLState::BufferObject> MakeAdoptedBuffer(ZeroCopyMockBackend& mock, GLenum target,
|
||||
GLuint& buffer) {
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(target, buffer);
|
||||
Vector<GLint> initial(kAdoptedInts);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) initial[i] = static_cast<GLint>(i);
|
||||
BufferData(target, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
EXPECT_NE(bufferObject, nullptr);
|
||||
if (bufferObject == nullptr) return nullptr;
|
||||
EXPECT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(static_cast<const void*>(bufferObject->MappedData()), static_cast<const void*>(mock.gpu.data()));
|
||||
mock.subDataCalls = 0;
|
||||
mock.flushCalls = 0;
|
||||
mock.respecifyCalls = 0;
|
||||
return bufferObject;
|
||||
}
|
||||
|
||||
const GLint* GpuInts(const ZeroCopyMockBackend& mock) {
|
||||
return reinterpret_cast<const GLint*>(mock.gpu.data());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(BufferTest, ANonPersistentReadWriteRangeMapOfAnAdoptedStoreLandsAtUnmap) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
// The conformance suite's shape: the whole store, READ|WRITE, then a full rewrite.
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
{0, kAdoptedInts * sizeof(GLint)}, BufferMappingAccessBit::Read | BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
// A non-persistent map is a staging copy, seeded from the adopted store...
|
||||
EXPECT_NE(static_cast<void*>(mapped), static_cast<void*>(mock.gpu.data()));
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) EXPECT_EQ(mapped[i], static_cast<GLint>(i));
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = 1000 + static_cast<GLint>(i);
|
||||
// ...that the store does not see until the unmap.
|
||||
EXPECT_EQ(GpuInts(mock)[0], 0);
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
EXPECT_FALSE(bufferObject->IsMapped());
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
EXPECT_EQ(GpuInts(mock)[i], 1000 + static_cast<GLint>(i)) << "int " << i;
|
||||
}
|
||||
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), mock.gpu.data(), mock.gpu.size()), 0);
|
||||
// The landing publishes the change for cached consumers...
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
// ...but dispatches no transfer op: the backend keeps no separate copy of an
|
||||
// adopted store, and its flush op would only upload the mapping onto itself.
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.acquireMapCalls, 1);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, GlMapBufferWriteOnlyAndReadWriteOfAnAdoptedStoreLandAtUnmap) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
// glMapBuffer(GL_WRITE_ONLY): the staging copy is still seeded (no invalidate bit),
|
||||
// so a partial write keeps the untouched ints.
|
||||
Uint64 serial = bufferObject->GetChangeSerial();
|
||||
auto* writeOnly = static_cast<GLint*>(bufferObject->AcquireMemory(true, false, true));
|
||||
ASSERT_NE(writeOnly, nullptr);
|
||||
EXPECT_NE(static_cast<void*>(writeOnly), static_cast<void*>(mock.gpu.data()));
|
||||
writeOnly[0] = 100;
|
||||
writeOnly[1] = 200;
|
||||
bufferObject->ReleaseMemory();
|
||||
EXPECT_EQ(GpuInts(mock)[0], 100);
|
||||
EXPECT_EQ(GpuInts(mock)[1], 200);
|
||||
EXPECT_EQ(GpuInts(mock)[2], 2);
|
||||
EXPECT_EQ(GpuInts(mock)[kAdoptedInts - 1], static_cast<GLint>(kAdoptedInts - 1));
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), serial);
|
||||
|
||||
// glMapBuffer(GL_READ_WRITE): reads see the previous landing, and the next one lands too.
|
||||
serial = bufferObject->GetChangeSerial();
|
||||
auto* readWrite = static_cast<GLint*>(bufferObject->AcquireMemory(true, true, true));
|
||||
ASSERT_NE(readWrite, nullptr);
|
||||
EXPECT_EQ(readWrite[0], 100);
|
||||
EXPECT_EQ(readWrite[1], 200);
|
||||
readWrite[2] = 300;
|
||||
bufferObject->ReleaseMemory();
|
||||
EXPECT_EQ(GpuInts(mock)[0], 100);
|
||||
EXPECT_EQ(GpuInts(mock)[1], 200);
|
||||
EXPECT_EQ(GpuInts(mock)[2], 300);
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), serial);
|
||||
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, AWriteMapInvalidatingAnAdoptedStoreLandsTheWholeRangeAtUnmap) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
{0, kAdoptedInts * sizeof(GLint)}, BufferMappingAccessBit::Write | BufferMappingAccessBit::InvalidateBuffer));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
EXPECT_NE(static_cast<void*>(mapped), static_cast<void*>(mock.gpu.data()));
|
||||
// The whole range is undefined by contract, so the application rewrites all of it.
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = -static_cast<GLint>(i) - 1;
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
EXPECT_EQ(GpuInts(mock)[i], -static_cast<GLint>(i) - 1) << "int " << i;
|
||||
}
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A range map at an offset off the alignment grid: the staging store is biased by the
|
||||
// offset's phase (see AcquireMemoryRange), and the landing has to read from the biased
|
||||
// start and write to the mapped offset - not from data(), not to 0.
|
||||
TEST_F(BufferTest, ARangeMapAtAnUnalignedOffsetOfAnAdoptedStoreLandsInPlace) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_ARRAY_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
// Ints 3..6, i.e. byte offset 12 - inside the first alignment, so the bias is non-zero.
|
||||
constexpr SizeT kFirst = 3;
|
||||
constexpr SizeT kCount = 4;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
ASSERT_NE(range.start % MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT, 0u);
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(range, BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
// Seeded from the right place...
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(mapped[i], static_cast<GLint>(kFirst + i));
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 500 + static_cast<GLint>(i);
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
// ...and landed in the right place, with everything outside the range untouched.
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? 500 + static_cast<GLint>(i - kFirst)
|
||||
: static_cast<GLint>(i);
|
||||
EXPECT_EQ(GpuInts(mock)[i], expected) << "int " << i;
|
||||
}
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// FLUSH_EXPLICIT on an adopted store: only the flushed bytes land, at the flush, and the
|
||||
// unmap lands nothing more - the application promised to flush what it wanted kept.
|
||||
TEST_F(BufferTest, AnExplicitFlushOfAWriteMapOfAnAdoptedStoreLandsOnlyTheFlushedBytes) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
// Ints 2..13 mapped (offset 8, off the grid again), all of them rewritten...
|
||||
constexpr SizeT kFirst = 2;
|
||||
constexpr SizeT kCount = 12;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Write | BufferMappingAccessBit::FlushExplicit));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 700 + static_cast<GLint>(i);
|
||||
// ...but only ints 5..8 (map-relative ints 3..6) flushed.
|
||||
constexpr SizeT kFlushFirst = 3;
|
||||
constexpr SizeT kFlushCount = 4;
|
||||
bufferObject->FlushMemoryRange(kFlushFirst * sizeof(GLint), kFlushCount * sizeof(GLint));
|
||||
const Uint64 flushSerial = bufferObject->GetChangeSerial();
|
||||
EXPECT_GT(flushSerial, baseSerial);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
|
||||
auto expectOnlyFlushedBytesLanded = [&](const char* when) {
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const Bool flushed = i >= kFirst + kFlushFirst && i < kFirst + kFlushFirst + kFlushCount;
|
||||
const GLint expected = flushed ? 700 + static_cast<GLint>(i - kFirst) : static_cast<GLint>(i);
|
||||
EXPECT_EQ(GpuInts(mock)[i], expected) << when << ": int " << i;
|
||||
}
|
||||
};
|
||||
expectOnlyFlushedBytesLanded("after the flush");
|
||||
|
||||
bufferObject->ReleaseMemory();
|
||||
expectOnlyFlushedBytesLanded("after the unmap");
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// The other kind of backend: one that takes the bytes for a GPU-ordered landing instead
|
||||
// of letting the frontend write the coherent mapping in place. The unmap hands it the
|
||||
// mapped offset and the bias-adjusted bytes, leaves the mapping alone, and marks a GPU
|
||||
// write outstanding so the next read reconciles through the readback.
|
||||
TEST_F(BufferTest, ABackendWithAResidentSubDataOpTakesTheUnmappedBytesForAGpuOrderedLanding) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
constexpr SizeT kFirst = 3;
|
||||
constexpr SizeT kCount = 5;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Read | BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 900 + static_cast<GLint>(i);
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
// The op got exactly the mapped range's bytes at the mapped offset...
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.residentWrites[0].offset, range.start);
|
||||
ASSERT_EQ(mock.residentWrites[0].bytes.size(), kCount * sizeof(GLint));
|
||||
const auto* handed = reinterpret_cast<const GLint*>(mock.residentWrites[0].bytes.data());
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(handed[i], 900 + static_cast<GLint>(i)) << "int " << i;
|
||||
// ...the mapping itself was not written through...
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) EXPECT_EQ(GpuInts(mock)[i], static_cast<GLint>(i)) << "int " << i;
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.readbackCalls, 0);
|
||||
|
||||
// ...and the pending flag makes the next read pull the landing back first.
|
||||
const auto* readBack = static_cast<const GLint*>(bufferObject->AcquireMemory(false, true, false));
|
||||
EXPECT_EQ(mock.readbackCalls, 1);
|
||||
EXPECT_TRUE(mock.residentWrites.empty());
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? 900 + static_cast<GLint>(i - kFirst)
|
||||
: static_cast<GLint>(i);
|
||||
EXPECT_EQ(readBack[i], expected) << "int " << i;
|
||||
}
|
||||
// A second read has nothing outstanding to reconcile.
|
||||
bufferObject->AcquireMemory(false, true, false);
|
||||
EXPECT_EQ(mock.readbackCalls, 1);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ABackendWithAResidentSubDataOpTakesAnExplicitlyFlushedRangeTheSameWay) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
constexpr SizeT kFirst = 2;
|
||||
constexpr SizeT kCount = 8;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Write | BufferMappingAccessBit::FlushExplicit));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 800 + static_cast<GLint>(i);
|
||||
constexpr SizeT kFlushFirst = 5;
|
||||
constexpr SizeT kFlushCount = 2;
|
||||
bufferObject->FlushMemoryRange(kFlushFirst * sizeof(GLint), kFlushCount * sizeof(GLint));
|
||||
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.residentWrites[0].offset, (kFirst + kFlushFirst) * sizeof(GLint));
|
||||
ASSERT_EQ(mock.residentWrites[0].bytes.size(), kFlushCount * sizeof(GLint));
|
||||
const auto* handed = reinterpret_cast<const GLint*>(mock.residentWrites[0].bytes.data());
|
||||
EXPECT_EQ(handed[0], 800 + static_cast<GLint>(kFlushFirst));
|
||||
EXPECT_EQ(handed[1], 800 + static_cast<GLint>(kFlushFirst + 1));
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
|
||||
// The unmap of a FLUSH_EXPLICIT map adds nothing.
|
||||
bufferObject->ReleaseMemory();
|
||||
EXPECT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// The CTS idiom end to end through the GL entry points: an SSBO made resident by a
|
||||
// draw, re-seeded with glMapBufferRange(READ|WRITE) + glUnmapBuffer.
|
||||
TEST_F(BufferTest, MapBufferRangeAndUnmapBufferReseedAnAdoptedShaderStorageBuffer) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
for (GLint pass = 1; pass <= 3; ++pass) {
|
||||
auto* mapped = static_cast<GLint*>(
|
||||
MapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)),
|
||||
GL_MAP_READ_BIT | GL_MAP_WRITE_BIT));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = pass * 100 + static_cast<GLint>(i);
|
||||
EXPECT_TRUE(UnmapBuffer(GL_SHADER_STORAGE_BUFFER));
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
EXPECT_EQ(GpuInts(mock)[i], pass * 100 + static_cast<GLint>(i)) << "pass " << pass << " int " << i;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// The control: a store the backend declined to adopt keeps the shadow model exactly as
|
||||
// before - the staging copy is written back into the shadow and the backend's flush op
|
||||
// carries the range down.
|
||||
TEST_F(BufferTest, ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend) {
|
||||
ZeroCopyMockBackend mock;
|
||||
mock.provideMap = false;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
Vector<GLint> initial(kAdoptedInts);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) initial[i] = static_cast<GLint>(i);
|
||||
BufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
mock.flushCalls = 0;
|
||||
mock.subDataCalls = 0;
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
constexpr SizeT kFirst = 3;
|
||||
constexpr SizeT kCount = 4;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(range, BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 600 + static_cast<GLint>(i);
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
EXPECT_EQ(mock.flushCalls, 1);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
const auto* shadow = reinterpret_cast<const GLint*>(bufferObject->MappedData());
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? 600 + static_cast<GLint>(i - kFirst)
|
||||
: static_cast<GLint>(i);
|
||||
EXPECT_EQ(shadow[i], expected) << "int " << i;
|
||||
}
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The other three CPU-sourced writes that share the unmap landing's route into an
|
||||
// adopted store - glBufferSubData, a clear, and a copy - on both kinds of backend: the
|
||||
// one that lets the frontend write the coherent mapping in place, and the one that takes
|
||||
// the bytes for a GPU-ordered landing, where the offset it is handed is the only thing
|
||||
// deciding where they end up.
|
||||
TEST_F(BufferTest, GlBufferSubDataIntoAnAdoptedStoreLandsInPlaceWithoutABackendTransfer) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_ARRAY_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
constexpr SizeT kFirst = 4;
|
||||
const GLint updated[] = {70, 71, 72};
|
||||
BufferSubData(GL_ARRAY_BUFFER, static_cast<GLintptr>(kFirst * sizeof(GLint)), sizeof(updated), updated);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + 3) ? updated[i - kFirst] : static_cast<GLint>(i);
|
||||
EXPECT_EQ(GpuInts(mock)[i], expected) << "int " << i;
|
||||
}
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ABackendWithAResidentSubDataOpTakesAGlBufferSubDataAtItsOffset) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_ARRAY_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
constexpr SizeT kFirst = 4;
|
||||
const GLint updated[] = {70, 71, 72};
|
||||
BufferSubData(GL_ARRAY_BUFFER, static_cast<GLintptr>(kFirst * sizeof(GLint)), sizeof(updated), updated);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.residentWrites[0].offset, kFirst * sizeof(GLint));
|
||||
ASSERT_EQ(mock.residentWrites[0].bytes.size(), sizeof(updated));
|
||||
EXPECT_EQ(std::memcmp(mock.residentWrites[0].bytes.data(), updated, sizeof(updated)), 0);
|
||||
// The mapping itself is left alone until the backend's ordered copy runs.
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) EXPECT_EQ(GpuInts(mock)[i], static_cast<GLint>(i)) << "int " << i;
|
||||
|
||||
const auto* readBack = static_cast<const GLint*>(bufferObject->AcquireMemory(false, true, false));
|
||||
EXPECT_EQ(mock.readbackCalls, 1);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + 3) ? updated[i - kFirst] : static_cast<GLint>(i);
|
||||
EXPECT_EQ(readBack[i], expected) << "int " << i;
|
||||
}
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, GlClearBufferSubDataRepeatsItsPatternThroughAnAdoptedStoreInPlace) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
// A four-byte pattern, so the repeat - not a memset - is what fills the range.
|
||||
constexpr SizeT kFirst = 5;
|
||||
constexpr SizeT kCount = 6;
|
||||
const GLint value = 0x0A0B0C0D;
|
||||
ClearBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_R32I, static_cast<GLintptr>(kFirst * sizeof(GLint)),
|
||||
static_cast<GLsizeiptr>(kCount * sizeof(GLint)), GL_RED_INTEGER, GL_INT, &value);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? value : static_cast<GLint>(i);
|
||||
EXPECT_EQ(GpuInts(mock)[i], expected) << "int " << i;
|
||||
}
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ABackendWithAResidentSubDataOpTakesTheExpandedClearPattern) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
constexpr SizeT kFirst = 5;
|
||||
constexpr SizeT kCount = 6;
|
||||
const GLint value = 0x0A0B0C0D;
|
||||
ClearBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_R32I, static_cast<GLintptr>(kFirst * sizeof(GLint)),
|
||||
static_cast<GLsizeiptr>(kCount * sizeof(GLint)), GL_RED_INTEGER, GL_INT, &value);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// The backend takes the FINAL bytes, so the pattern arrives already repeated.
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.residentWrites[0].offset, kFirst * sizeof(GLint));
|
||||
ASSERT_EQ(mock.residentWrites[0].bytes.size(), kCount * sizeof(GLint));
|
||||
const auto* handed = reinterpret_cast<const GLint*>(mock.residentWrites[0].bytes.data());
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(handed[i], value) << "int " << i;
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) EXPECT_EQ(GpuInts(mock)[i], static_cast<GLint>(i)) << "int " << i;
|
||||
|
||||
const auto* readBack = static_cast<const GLint*>(bufferObject->AcquireMemory(false, true, false));
|
||||
EXPECT_EQ(mock.readbackCalls, 1);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? value : static_cast<GLint>(i);
|
||||
EXPECT_EQ(readBack[i], expected) << "int " << i;
|
||||
}
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A plain (never adopted) buffer of kAdoptedInts ints, each `bias` above its index,
|
||||
// bound to `target` as the source of a copy.
|
||||
GLuint MakeCopySource(GLenum target, GLint bias) {
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(target, buffer);
|
||||
Vector<GLint> bytes(kAdoptedInts);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) bytes[i] = bias + static_cast<GLint>(i);
|
||||
BufferData(target, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), bytes.data(), GL_STATIC_DRAW);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
return buffer;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(BufferTest, GlCopyBufferSubDataIntoAnAdoptedStoreLandsAtTheDestinationOffset) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
GLuint destination = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_COPY_WRITE_BUFFER, destination);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const GLuint source = MakeCopySource(GL_COPY_READ_BUFFER, 900);
|
||||
|
||||
// Deliberately different source and destination offsets: only the destination one
|
||||
// may decide where the bytes land.
|
||||
constexpr SizeT kSrcFirst = 1;
|
||||
constexpr SizeT kDstFirst = 6;
|
||||
constexpr SizeT kCount = 3;
|
||||
CopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, static_cast<GLintptr>(kSrcFirst * sizeof(GLint)),
|
||||
static_cast<GLintptr>(kDstFirst * sizeof(GLint)),
|
||||
static_cast<GLsizeiptr>(kCount * sizeof(GLint)));
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) {
|
||||
const GLint expected = (i >= kDstFirst && i < kDstFirst + kCount)
|
||||
? 900 + static_cast<GLint>(kSrcFirst + i - kDstFirst)
|
||||
: static_cast<GLint>(i);
|
||||
EXPECT_EQ(GpuInts(mock)[i], expected) << "int " << i;
|
||||
}
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
|
||||
GLuint toDelete[] = {destination, source};
|
||||
DeleteBuffers(2, toDelete);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ABackendWithAResidentSubDataOpTakesACopyAtTheDestinationOffset) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint destination = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_COPY_WRITE_BUFFER, destination);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const GLuint source = MakeCopySource(GL_COPY_READ_BUFFER, 900);
|
||||
|
||||
constexpr SizeT kSrcFirst = 1;
|
||||
constexpr SizeT kDstFirst = 6;
|
||||
constexpr SizeT kCount = 3;
|
||||
CopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, static_cast<GLintptr>(kSrcFirst * sizeof(GLint)),
|
||||
static_cast<GLintptr>(kDstFirst * sizeof(GLint)),
|
||||
static_cast<GLsizeiptr>(kCount * sizeof(GLint)));
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.residentWrites[0].offset, kDstFirst * sizeof(GLint));
|
||||
ASSERT_EQ(mock.residentWrites[0].bytes.size(), kCount * sizeof(GLint));
|
||||
const auto* handed = reinterpret_cast<const GLint*>(mock.residentWrites[0].bytes.data());
|
||||
for (SizeT i = 0; i < kCount; ++i) {
|
||||
EXPECT_EQ(handed[i], 900 + static_cast<GLint>(kSrcFirst + i)) << "int " << i;
|
||||
}
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) EXPECT_EQ(GpuInts(mock)[i], static_cast<GLint>(i)) << "int " << i;
|
||||
|
||||
GLuint toDelete[] = {destination, source};
|
||||
DeleteBuffers(2, toDelete);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A PERSISTENT map of an adopted store is the one write shape that needs no landing at
|
||||
// all: it wrote the coherent mapping in place. Its explicit flush therefore publishes the
|
||||
// change and dispatches nothing - not the backend's flush op (whose upload would be the
|
||||
// mapping onto itself) and not the resident landing op (whose bytes are already there).
|
||||
TEST_F(BufferTest, AnExplicitFlushOfAPersistentMapOfAnAdoptedStoreOnlyPublishesTheChange) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
constexpr SizeT kFirst = 2;
|
||||
constexpr SizeT kCount = 8;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent |
|
||||
BufferMappingAccessBit::FlushExplicit));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
// The application writes the store itself: the map IS the adopted memory.
|
||||
EXPECT_EQ(static_cast<void*>(mapped), static_cast<void*>(mock.gpu.data() + range.start));
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 400 + static_cast<GLint>(i);
|
||||
|
||||
constexpr SizeT kFlushFirst = 3;
|
||||
constexpr SizeT kFlushCount = 2;
|
||||
bufferObject->FlushMemoryRange(kFlushFirst * sizeof(GLint), kFlushCount * sizeof(GLint));
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_TRUE(mock.residentWrites.empty());
|
||||
EXPECT_TRUE(bufferObject->HasDefinedContent());
|
||||
// Every byte the map wrote is in the store, flushed or not - it was written there.
|
||||
for (SizeT i = 0; i < kCount; ++i) {
|
||||
EXPECT_EQ(GpuInts(mock)[kFirst + i], 400 + static_cast<GLint>(i)) << "int " << i;
|
||||
}
|
||||
|
||||
const Uint64 flushSerial = bufferObject->GetChangeSerial();
|
||||
bufferObject->ReleaseMemory();
|
||||
EXPECT_EQ(bufferObject->GetChangeSerial(), flushSerial); // the unmap adds nothing
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_TRUE(mock.residentWrites.empty());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A flush of nothing wrote no byte, so it may not report the store as written: an
|
||||
// orphaning respecification prices a "has content" store as a full-size upload.
|
||||
TEST_F(BufferTest, AZeroLengthExplicitFlushOfAnAdoptedStoreLeavesItUndefined) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
BufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), nullptr,
|
||||
GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
ASSERT_FALSE(bufferObject->HasDefinedContent());
|
||||
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
const Uint64 baseSerial = bufferObject->GetChangeSerial();
|
||||
|
||||
auto* mapped = bufferObject->AcquireMemoryRange({0, kAdoptedInts * sizeof(GLint)},
|
||||
BufferMappingAccessBit::Write |
|
||||
BufferMappingAccessBit::Persistent |
|
||||
BufferMappingAccessBit::FlushExplicit);
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
bufferObject->FlushMemoryRange(0, 0);
|
||||
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
|
||||
EXPECT_FALSE(bufferObject->HasDefinedContent());
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// A write map that discards the range it maps reads nothing of the store: its staging
|
||||
// copy is not seeded from it. Reconciling an adopted store at map time would run the
|
||||
// backend's drain-and-wait for no reader, once per map, on the streaming arena the
|
||||
// adoption exists to keep cheap - so it is deferred, not dropped: the first map that DOES
|
||||
// read the bytes still pays for it, and every queued landing is still applied, in order.
|
||||
TEST_F(BufferTest, AWriteMapThatDiscardsWhatItMapsDoesNotReconcileAnAdoptedStoreAtMapTime) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_ARRAY_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
// An earlier write is queued for its GPU-ordered landing...
|
||||
const GLint firstInt = 55;
|
||||
BufferSubData(GL_ARRAY_BUFFER, 0, sizeof(firstInt), &firstInt);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
ASSERT_EQ(mock.residentWrites.size(), 1u);
|
||||
EXPECT_EQ(mock.readbackCalls, 0);
|
||||
|
||||
// ...and the map that discards its range does not wait for it.
|
||||
constexpr SizeT kFirst = 8;
|
||||
constexpr SizeT kCount = 4;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Write | BufferMappingAccessBit::InvalidateRange));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
EXPECT_EQ(mock.readbackCalls, 0);
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 300 + static_cast<GLint>(i);
|
||||
bufferObject->ReleaseMemory();
|
||||
ASSERT_EQ(mock.residentWrites.size(), 2u);
|
||||
|
||||
// The first read reconciles both landings, oldest first.
|
||||
const auto* readBack = static_cast<const GLint*>(bufferObject->AcquireMemory(false, true, false));
|
||||
EXPECT_EQ(mock.readbackCalls, 1);
|
||||
EXPECT_EQ(readBack[0], firstInt);
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(readBack[kFirst + i], 300 + static_cast<GLint>(i)) << "int " << i;
|
||||
|
||||
// The control: a map that keeps what it maps still reconciles before seeding.
|
||||
BufferSubData(GL_ARRAY_BUFFER, 0, sizeof(firstInt), &firstInt);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto* seeded = static_cast<GLint*>(
|
||||
bufferObject->AcquireMemoryRange(range, BufferMappingAccessBit::Read | BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(seeded, nullptr);
|
||||
EXPECT_EQ(mock.readbackCalls, 2);
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(seeded[i], 300 + static_cast<GLint>(i)) << "int " << i;
|
||||
bufferObject->ReleaseMemory();
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// Adoption releases the CPU shadow, and a persistent map that did not itself adopt
|
||||
// (FLUSH_EXPLICIT is excluded from adoption) handed the application a pointer into that
|
||||
// shadow which GL keeps valid while the buffer is drawn with - which is exactly when a
|
||||
// storage binding asks for residency. So a mapped buffer keeps the shadow model.
|
||||
TEST_F(BufferTest, AStorageBindingDoesNotAdoptTheStoreWhileTheApplicationHoldsAMapping) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
Vector<GLint> initial(kAdoptedInts);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) initial[i] = static_cast<GLint>(i);
|
||||
BufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
const auto* shadowBase = bufferObject->MappedData();
|
||||
|
||||
constexpr SizeT kFirst = 2;
|
||||
constexpr SizeT kCount = 4;
|
||||
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
|
||||
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(
|
||||
range, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent |
|
||||
BufferMappingAccessBit::FlushExplicit));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
ASSERT_EQ(static_cast<const void*>(mapped), static_cast<const void*>(shadowBase + range.start));
|
||||
|
||||
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_EQ(mock.acquireMapCalls, 0);
|
||||
// The application's pointer is still the store's: it survived the binding.
|
||||
EXPECT_EQ(static_cast<const void*>(bufferObject->MappedData()), static_cast<const void*>(shadowBase));
|
||||
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 250 + static_cast<GLint>(i);
|
||||
bufferObject->FlushMemoryRange(0, kCount * sizeof(GLint));
|
||||
EXPECT_EQ(mock.flushCalls, 1);
|
||||
const auto* shadowInts = reinterpret_cast<const GLint*>(bufferObject->MappedData());
|
||||
for (SizeT i = 0; i < kCount; ++i) EXPECT_EQ(shadowInts[kFirst + i], 250 + static_cast<GLint>(i));
|
||||
|
||||
// Unmapped, the next binding adopts as usual.
|
||||
bufferObject->ReleaseMemory();
|
||||
EXPECT_TRUE(bufferObject->EnsureGpuResidentStorage());
|
||||
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
// Respecifying a store hands any adoption back and replaces the bytes, so the staged
|
||||
// bytes of a map that is still live have nowhere to land: copying a whole mapped range
|
||||
// into storage that is released on the next line is pure waste.
|
||||
TEST_F(BufferTest, RespecifyingAStoreWhileItIsMappedDoesNotLandTheStagedBytesIntoIt) {
|
||||
ZeroCopyMockBackend mock;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kResidentSubDataMockOps);
|
||||
GLuint buffer = 0;
|
||||
auto bufferObject = MakeAdoptedBuffer(mock, GL_ARRAY_BUFFER, buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
|
||||
auto* mapped = static_cast<GLint*>(
|
||||
bufferObject->AcquireMemoryRange({0, kAdoptedInts * sizeof(GLint)}, BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = 1234;
|
||||
|
||||
bufferObject->Respecify(kAdoptedInts * sizeof(GLint), nullptr);
|
||||
EXPECT_TRUE(mock.residentWrites.empty());
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.respecifyCalls, 1);
|
||||
EXPECT_FALSE(bufferObject->IsMapped());
|
||||
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
|
||||
EXPECT_FALSE(bufferObject->HasDefinedContent());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown) {
|
||||
ZeroCopyMockBackend mock;
|
||||
mock.provideMap = false;
|
||||
g_zeroCopyMock = &mock;
|
||||
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
|
||||
|
||||
GLuint buffer = 0;
|
||||
GenBuffers(1, &buffer);
|
||||
BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
Vector<GLint> initial(kAdoptedInts, 7);
|
||||
BufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
mock.flushCalls = 0;
|
||||
mock.subDataCalls = 0;
|
||||
mock.respecifyCalls = 0;
|
||||
|
||||
auto* mapped = static_cast<GLint*>(
|
||||
bufferObject->AcquireMemoryRange({0, kAdoptedInts * sizeof(GLint)}, BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = 1234;
|
||||
|
||||
bufferObject->Respecify(kAdoptedInts * sizeof(GLint), nullptr);
|
||||
EXPECT_EQ(mock.flushCalls, 0);
|
||||
EXPECT_EQ(mock.subDataCalls, 0);
|
||||
EXPECT_EQ(mock.respecifyCalls, 1);
|
||||
EXPECT_FALSE(bufferObject->IsMapped());
|
||||
EXPECT_FALSE(bufferObject->HasDefinedContent());
|
||||
|
||||
DeleteBuffers(1, &buffer);
|
||||
g_zeroCopyMock = nullptr;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ set(LINK_LIBRARIES
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
if (MSVC)
|
||||
# The GL headers declare gl* as dllimport on Windows, so any test that pulls
|
||||
# GetProcAddress.cpp out of the static library references __imp_gl*, which only
|
||||
# resolves when the in-library entry-point definitions are part of the link.
|
||||
# Applies to every test executable below, the way the DirectVulkan and
|
||||
# integration test targets already do it for themselves.
|
||||
add_link_options(/WHOLEARCHIVE:MobileGL_s)
|
||||
endif()
|
||||
|
||||
add_subdirectory(BackendLoader)
|
||||
add_subdirectory(Buffer)
|
||||
# The heap-address-is-not-an-identity invariant the backends' per-object memos
|
||||
|
||||
@@ -1034,14 +1034,69 @@ namespace {
|
||||
if (index < kRecordedDrawBuffers) g_driverIndexedColorMasks[index] = {true, r, g, b, a};
|
||||
}
|
||||
|
||||
// What the blend block of SyncRenderState pushed. Enough to answer the two questions the
|
||||
// dual-source cases ask: is blending on for a draw buffer, and which factor enums reached
|
||||
// the driver.
|
||||
struct RecordedBlend {
|
||||
Bool enabled = false;
|
||||
Bool enableSeen = false;
|
||||
Bool factorsSeen = false;
|
||||
GLenum srcRGB = 0, dstRGB = 0, srcAlpha = 0, dstAlpha = 0;
|
||||
};
|
||||
RecordedBlend g_driverBlend[kRecordedDrawBuffers];
|
||||
|
||||
void ResetRecordedBlend() {
|
||||
for (auto& recorded : g_driverBlend) recorded = {};
|
||||
}
|
||||
|
||||
void RecordBlendEnable(Bool enabled) {
|
||||
for (auto& recorded : g_driverBlend) {
|
||||
recorded.enabled = enabled;
|
||||
recorded.enableSeen = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RecordBlendFactors(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
for (auto& recorded : g_driverBlend) {
|
||||
recorded.factorsSeen = true;
|
||||
recorded.srcRGB = srcRGB;
|
||||
recorded.dstRGB = dstRGB;
|
||||
recorded.srcAlpha = srcAlpha;
|
||||
recorded.dstAlpha = dstAlpha;
|
||||
}
|
||||
}
|
||||
|
||||
void StubViewport(GLint, GLint, GLsizei, GLsizei) {}
|
||||
void StubScissor(GLint, GLint, GLsizei, GLsizei) {}
|
||||
void StubEnable(GLenum) {}
|
||||
void StubDisable(GLenum) {}
|
||||
void StubEnablei(GLenum, GLuint) {}
|
||||
void StubDisablei(GLenum, GLuint) {}
|
||||
void StubBlendFuncSeparate(GLenum, GLenum, GLenum, GLenum) {}
|
||||
void StubBlendFuncSeparatei(GLuint, GLenum, GLenum, GLenum, GLenum) {}
|
||||
void StubEnable(GLenum cap) {
|
||||
if (cap == GL_BLEND) RecordBlendEnable(true);
|
||||
}
|
||||
void StubDisable(GLenum cap) {
|
||||
if (cap == GL_BLEND) RecordBlendEnable(false);
|
||||
}
|
||||
void StubEnablei(GLenum cap, GLuint index) {
|
||||
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
|
||||
g_driverBlend[index].enabled = true;
|
||||
g_driverBlend[index].enableSeen = true;
|
||||
}
|
||||
}
|
||||
void StubDisablei(GLenum cap, GLuint index) {
|
||||
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
|
||||
g_driverBlend[index].enabled = false;
|
||||
g_driverBlend[index].enableSeen = true;
|
||||
}
|
||||
}
|
||||
void StubBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
RecordBlendFactors(srcRGB, dstRGB, srcAlpha, dstAlpha);
|
||||
}
|
||||
void StubBlendFuncSeparatei(GLuint index, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
if (index >= kRecordedDrawBuffers) return;
|
||||
g_driverBlend[index].factorsSeen = true;
|
||||
g_driverBlend[index].srcRGB = srcRGB;
|
||||
g_driverBlend[index].dstRGB = dstRGB;
|
||||
g_driverBlend[index].srcAlpha = srcAlpha;
|
||||
g_driverBlend[index].dstAlpha = dstAlpha;
|
||||
}
|
||||
void StubBlendEquationSeparate(GLenum, GLenum) {}
|
||||
void StubBlendEquationSeparatei(GLuint, GLenum, GLenum) {}
|
||||
void StubBlendColor(GLfloat, GLfloat, GLfloat, GLfloat) {}
|
||||
@@ -1066,7 +1121,9 @@ namespace {
|
||||
// into a driver that this process never made current.
|
||||
class ScopedRenderStateDriverStubs {
|
||||
public:
|
||||
ScopedRenderStateDriverStubs():
|
||||
// dualSourceBlendSupported models GL_EXT_blend_func_extended on the ES driver, which is
|
||||
// the one capability in here that a real device is commonly WITHOUT.
|
||||
explicit ScopedRenderStateDriverStubs(Bool dualSourceBlendSupported = true):
|
||||
m_funcs(MG_Backend::DirectGLES::g_GLESFuncs), m_caps(MG_Backend::DirectGLES::g_GLESCapabilities) {
|
||||
auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
|
||||
gl = MG_External::GLESFunctionsTable{};
|
||||
@@ -1102,9 +1159,10 @@ namespace {
|
||||
caps.SupportsIndexedColorMask = true;
|
||||
caps.SupportsSrgbWriteControl = false;
|
||||
caps.SupportsPolygonMode = false;
|
||||
caps.SupportsDualSourceBlend = true;
|
||||
caps.SupportsDualSourceBlend = dualSourceBlendSupported;
|
||||
|
||||
ResetRecordedColorMasks();
|
||||
ResetRecordedBlend();
|
||||
// The viewport and scissor blocks fall back to querying the surface size when the
|
||||
// frontend's rectangle is degenerate, and there is no surface in this process.
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
|
||||
@@ -1119,6 +1177,11 @@ namespace {
|
||||
// The shadow now describes pushes that went to the stubs, not to any driver.
|
||||
MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState();
|
||||
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// Blend state is per-CONTEXT and the context outlives the fixture, so a case that
|
||||
// enabled blending or asked for an exotic factor has to put it back or every later
|
||||
// case in this binary inherits it.
|
||||
MG_Impl::GLImpl::Disable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_ONE, GL_ZERO);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1253,6 +1316,143 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --- Dual-source blending without GL_EXT_blend_func_extended ------------------------------------
|
||||
//
|
||||
// GL_SRC1_* blend factors are core GL since 3.3, GLES core has nothing equivalent, and the ES
|
||||
// driver may or may not carry GL_EXT_blend_func_extended. When it does, the factors translate and
|
||||
// blend properly - the positive case below. When it does not, the blend block used to
|
||||
// THROW_EXCEPTION, which is a plain `throw` (MG_Util/Types.h) with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so it unwound out through the C GL ABI and killed the process over one unsupported
|
||||
// blend factor. It now DECLINES: the draw buffer is pushed with blending off and neutral One/Zero
|
||||
// factors, and the loss is logged once.
|
||||
//
|
||||
// Both halves are asserted at the seam that matters - what the ES driver is actually handed -
|
||||
// because a GL_SRC1_* enum reaching a driver without the extension is the other failure mode: the
|
||||
// driver answers GL_INVALID_ENUM, keeps whatever factors were set before, and mis-blends silently.
|
||||
|
||||
TEST_F(FramebufferTest, DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/true);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_SRC1_* is core since 3.3; glBlendFunc must take it";
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled) << "nothing may decline a blend the driver can do";
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].srcAlpha, static_cast<GLenum>(GL_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/false);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< "the FRONTEND accepts the factor whatever the driver can do - the decline is a backend decision";
|
||||
ResetRecordedBlend();
|
||||
|
||||
// The whole point: this used to be `throw std::runtime_error` straight through the GL ABI.
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].enableSeen) << "the blend enable still has to be pushed";
|
||||
EXPECT_FALSE(g_driverBlend[0].enabled) << "a blend the driver cannot do is declined, not attempted";
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA))
|
||||
<< "draw buffer " << i << ": no GL_SRC1_* enum may reach a driver without the extension";
|
||||
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].srcAlpha, static_cast<GLenum>(GL_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
}
|
||||
|
||||
// The decline is scoped to the offending factor, not to blending as a whole: an ordinary
|
||||
// blend on the same driver still goes through, and the SAME sync that declined the first one
|
||||
// is what has to push it.
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled);
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC_ALPHA));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC_ALPHA));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The half the first version of the decline missed: the FACTOR push is not gated on Enabled, so
|
||||
// GL_BLEND being OFF does not keep a GL_SRC1_* enum away from a driver that cannot parse it. This
|
||||
// is the sequence - `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` then any draw or clear -
|
||||
// and it needs no dual-source shader at all, which is why it survived both the enabled-path unit
|
||||
// case above and the integration scenario (that one skips on exactly the extension-less lanes this
|
||||
// concerns, because its probe needs a dual-source program to render).
|
||||
//
|
||||
// What a leaked enum costs: the driver answers GL_INVALID_ENUM and keeps its previous factors, so
|
||||
// the error sits in the ES context's own queue for the next internal `glGetError() == GL_NO_ERROR`
|
||||
// probe to read as its own failure, and this backend's shadow records factors the context rejected.
|
||||
TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/false);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
ResetRecordedBlend();
|
||||
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
|
||||
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
EXPECT_FALSE(g_driverBlend[i].enabled) << "draw buffer " << i << ": blending was never enabled";
|
||||
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA))
|
||||
<< "draw buffer " << i
|
||||
<< ": a GL_SRC1_* enum must not reach a driver without the extension even with GL_BLEND off";
|
||||
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].srcAlpha, static_cast<GLenum>(GL_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
}
|
||||
|
||||
// A clear reaches the same block by the same route (SyncRenderState(forColorClear=true)), and
|
||||
// the flag only steers the alpha-widen colour mask, so it must not reopen this either.
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
ResetRecordedBlend();
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true));
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR)) << "draw buffer " << i;
|
||||
}
|
||||
|
||||
// And the shadow records what was PUSHED, not what the frontend holds - otherwise the next
|
||||
// switch to an ordinary factor diffs against state the ES context never received.
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled) << "the enable has to be pushed - the shadow said 'off' because it was";
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC_ALPHA));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC_ALPHA));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The capable driver is unaffected by the ungating: GL_BLEND off with SRC1 factors set is a state
|
||||
// an application may legitimately hold, and the factors still have to reach a driver that parses
|
||||
// them - otherwise the next glEnable(GL_BLEND) would blend against neutralised state.
|
||||
TEST_F(FramebufferTest, DualSourceFactorsWithBlendingDisabledStillReachACapableDriver) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/true);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_FALSE(g_driverBlend[0].enabled);
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) ---------------------------------
|
||||
//
|
||||
// Four of them were missing from the bound-target path while its DSA sibling
|
||||
|
||||
@@ -23,6 +23,17 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
# This test compiles library sources of its own; pulling the whole static
|
||||
# library in as well (the directory-wide MG_Test link option) would define
|
||||
# them twice, so that option is dropped for this one target.
|
||||
get_target_property(_program_util_link_options ProgramUtilTest LINK_OPTIONS)
|
||||
if (_program_util_link_options)
|
||||
list(REMOVE_ITEM _program_util_link_options /WHOLEARCHIVE:MobileGL_s)
|
||||
set_target_properties(ProgramUtilTest PROPERTIES LINK_OPTIONS "${_program_util_link_options}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
ProgramTest
|
||||
ProgramTest.cpp
|
||||
@@ -180,6 +191,22 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
TessellationLinkTest
|
||||
TessellationLinkTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(TessellationLinkTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
TessellationLinkTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
ProgramPipelineCompositeTest
|
||||
ProgramPipelineCompositeTest.cpp
|
||||
@@ -229,6 +256,7 @@ gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramPipelineCompositeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(TessellationLinkTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
||||
# compile pool so there is something in flight to race against.
|
||||
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
|
||||
@@ -4023,6 +4023,131 @@ void main() { mgColor = vec4(1.0); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// Two fragment outputs on ONE location with DIFFERENT colour indices is not an aliasing error -
|
||||
// it is dual-source blending (GL 4.6 core 11.1.3 / ARB_blend_func_extended, core since 3.3), and
|
||||
// the GL_SRC1_* blend factors have nothing to read without it. The link-time aliasing check keyed
|
||||
// on the colour number alone, so every such program failed to link with "alias color number 0"
|
||||
// and the whole feature was unreachable from shader-side GLSL.
|
||||
TEST_F(ProgramTest, FragmentOutputsMayShareALocationWhenTheirColorIndexDiffers) {
|
||||
constexpr const char* dualSourceFs = R"(#version 460 core
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main() { fragColor0 = vec4(1.0); fragColor1 = vec4(0.5); }
|
||||
)";
|
||||
const GLuint program = LinkStages({{GL_VERTEX_SHADER, kPassthroughVs}, {GL_FRAGMENT_SHADER, dualSourceFs}});
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
ASSERT_EQ(linkStatus, GL_TRUE) << [&] {
|
||||
char log[512] = "";
|
||||
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
return std::string(log);
|
||||
}();
|
||||
// Both outputs are active and both sit on colour number 0 - which is the shape that used to be
|
||||
// refused. (glGetFragDataIndex still answers 0 for the index-1 output: it reports only what
|
||||
// glBindFragDataLocationIndexed bound, and reflecting the shader-side qualifier is a separate
|
||||
// gap, so it is deliberately not asserted here.)
|
||||
EXPECT_EQ(GetFragDataLocation(program, "fragColor0"), 0);
|
||||
EXPECT_EQ(GetFragDataLocation(program, "fragColor1"), 0);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The check it must NOT stop making: two outputs on the same colour number AND the same index
|
||||
// really do alias, and that link has to fail. Aliased through glBindFragDataLocation rather than
|
||||
// through two `layout(location = 0)` qualifiers on purpose - the qualifier form is caught by
|
||||
// glslang at COMPILE time, so it would never reach the link-time rule this pins.
|
||||
TEST_F(ProgramTest, FragmentOutputsSharingAColorNumberAtTheSameIndexStillFailToLink) {
|
||||
constexpr const char* twoOutputFs = R"(#version 460 core
|
||||
out vec4 fragColorA;
|
||||
out vec4 fragColorB;
|
||||
void main() { fragColorA = vec4(1.0); fragColorB = vec4(0.5); }
|
||||
)";
|
||||
const GLuint program = CreateProgram();
|
||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(vs, 1, &kPassthroughVs, nullptr);
|
||||
CompileShader(vs);
|
||||
AttachShader(program, vs);
|
||||
DeleteShader(vs);
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &twoOutputFs, nullptr);
|
||||
CompileShader(fs);
|
||||
AttachShader(program, fs);
|
||||
DeleteShader(fs);
|
||||
|
||||
BindFragDataLocation(program, 0, "fragColorA");
|
||||
BindFragDataLocation(program, 0, "fragColorB");
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_TRUE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
EXPECT_EQ(linkStatus, GL_FALSE);
|
||||
char infoLog[512] = "";
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
EXPECT_NE(std::string(infoLog).find("alias color number"), std::string::npos) << infoLog;
|
||||
|
||||
// ...and the same pair separated by the colour INDEX links, which is the whole point of the
|
||||
// key being a pair.
|
||||
BindFragDataLocationIndexed(program, 0, 1, "fragColorB");
|
||||
LinkProgram(program);
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
EXPECT_EQ(linkStatus, GL_TRUE) << [&] {
|
||||
char log[512] = "";
|
||||
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
return std::string(log);
|
||||
}();
|
||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// An API colour index of ZERO is "no override", not "index 0". glBindFragDataLocation is
|
||||
// glBindFragDataLocationIndexed with index 0 (GL_Program.cpp), so the blanket-bind pattern -
|
||||
// portable code that binds every output name it knows about, without caring about dual-source -
|
||||
// writes a real 0 into the frag-data index map for an output whose shader qualifier says 1.
|
||||
// Reading that 0 as an override collapsed both outputs onto slot (0,0) and failed the link as an
|
||||
// alias, while the IO resolver had left the qualifier at 1 and the emitted SPIR-V still carried
|
||||
// Index 1 - validation rejecting a program the backend had already built correctly.
|
||||
//
|
||||
// The rule pinned here is the codebase's (non-zero API index wins, zero falls back to the shader
|
||||
// qualifier), which is also what GL 4.6 core 15.2.3 gives for THIS shape: a shader layout
|
||||
// qualifier is used and the bound value ignored.
|
||||
TEST_F(ProgramTest, AnApiColorIndexOfZeroDoesNotOverrideTheShaderIndexQualifier) {
|
||||
constexpr const char* dualSourceFs = R"(#version 460 core
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main() { fragColor0 = vec4(1.0); fragColor1 = vec4(0.5); }
|
||||
)";
|
||||
const GLuint program = CreateProgram();
|
||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(vs, 1, &kPassthroughVs, nullptr);
|
||||
CompileShader(vs);
|
||||
AttachShader(program, vs);
|
||||
DeleteShader(vs);
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &dualSourceFs, nullptr);
|
||||
CompileShader(fs);
|
||||
AttachShader(program, fs);
|
||||
DeleteShader(fs);
|
||||
|
||||
// The blanket bind: colour number 0, index 0, on the output the shader put at index 1.
|
||||
BindFragDataLocation(program, 0, "fragColor0");
|
||||
BindFragDataLocation(program, 0, "fragColor1");
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
EXPECT_EQ(linkStatus, GL_TRUE) << [&] {
|
||||
char log[512] = "";
|
||||
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
return std::string(log);
|
||||
}();
|
||||
|
||||
// The explicit indexed form with a NON-zero index is still an override, and still links.
|
||||
BindFragDataLocationIndexed(program, 0, 1, "fragColor1");
|
||||
LinkProgram(program);
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
EXPECT_EQ(linkStatus, GL_TRUE);
|
||||
EXPECT_EQ(GetFragDataIndex(program, "fragColor1"), 1);
|
||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, GetProgramivReportsTheGeometryStageLinkProperties) {
|
||||
constexpr const char* gs = R"(#version 460 core
|
||||
layout(triangles, invocations = 3) in;
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// MobileGL - MobileGL/MG_Test/Program/TessellationLinkTest.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
|
||||
|
||||
// Link-time properties of programs that carry a tessellation control stage. GPU-free:
|
||||
// everything asserted here is a property of the link, not of any driver.
|
||||
//
|
||||
// Two independent defects live here, both found by KHR-GL4x.tessellation_shader:
|
||||
//
|
||||
// (1) The transform-feedback capture stage. GL 4.6 core 11 makes the tessellation CONTROL
|
||||
// shader a vertex-processing stage like the other three, so in a separable program whose
|
||||
// only stage is a TCS it is the LAST vertex-processing stage and therefore the capture
|
||||
// stage - such a program must link with transform-feedback varyings requested. MobileGL
|
||||
// searched {geometry, tessellation evaluation, vertex} only and refused the link with
|
||||
// "Transform feedback varyings requested but the program has no vertex-processing stage",
|
||||
// failing KHR-GL4x.tessellation_shader.single.xfb_captures_data_from_correct_stage on all
|
||||
// three API versions (esextcTessellationShaderXFB.cpp:390-416 passes should_succeed=true
|
||||
// for a non-ES context; ES demands the opposite, which is why the new arm is documented as
|
||||
// desktop-GL-only at the search site).
|
||||
//
|
||||
// (2) `patch out T name[N]` against `patch in T name[N]`. Legal, identically spelled on both
|
||||
// sides, and rejected until the glslang fork was re-pinned at d89cf443 - see the last case,
|
||||
// which carries the diagnosis and now guards the pin.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ios>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
||||
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Impl::GLImpl;
|
||||
|
||||
namespace {
|
||||
class TessellationLinkTest: public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::string ShaderLog(GLuint shader) {
|
||||
char log[4096] = "";
|
||||
GetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
return std::string(log);
|
||||
}
|
||||
|
||||
std::string LinkLog(GLuint program) {
|
||||
char log[4096] = "";
|
||||
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
return std::string(log);
|
||||
}
|
||||
|
||||
GLint Programiv(GLuint program, GLenum pname) {
|
||||
GLint value = -1;
|
||||
GetProgramiv(program, pname, &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Attaches one compiled shader of each requested stage. Compilation is asserted, so a
|
||||
// failure here is a shader bug in the test rather than a link result.
|
||||
GLuint MakeProgram(const std::vector<std::pair<GLenum, const char*>>& stages, Bool separable) {
|
||||
const GLuint program = CreateProgram();
|
||||
if (separable) {
|
||||
ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
}
|
||||
for (const auto& [type, source]: stages) {
|
||||
const GLuint shader = CreateShader(type);
|
||||
ShaderSource(shader, 1, &source, nullptr);
|
||||
CompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE) << "stage 0x" << std::hex << type << "\n" << ShaderLog(shader);
|
||||
AttachShader(program, shader);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// The conformance suite's own tessellation control shader
|
||||
// (esextcTessellationShaderXFB.cpp:360-381), with the ES-only ${...} expansions dropped -
|
||||
// on a desktop context they expand to nothing.
|
||||
constexpr const char* kCtsTessControl = R"(#version 460 core
|
||||
layout (vertices=4) out;
|
||||
|
||||
in BLOCK_INOUT { vec4 value; } user_in[];
|
||||
out BLOCK_INOUT { vec4 value; } user_out[];
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_out [gl_InvocationID].gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
user_out [gl_InvocationID].value = vec4(2.0, 3.0, 4.0, 5.0);
|
||||
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// A tessellation control shader IS a vertex-processing stage (GL 4.6 core 11), and in a
|
||||
// TCS-only separable program it is the last one - so it is the capture stage and the link
|
||||
// must succeed with the block member resolved against ITS outputs.
|
||||
TEST_F(TessellationLinkTest, TcsOnlySeparableProgramWithXfbVaryingsLinks) {
|
||||
const GLuint program = MakeProgram({{GL_TESS_CONTROL_SHADER, kCtsTessControl}}, /*separable=*/true);
|
||||
const GLchar* const varyings[1] = {"BLOCK_INOUT.value"};
|
||||
TransformFeedbackVaryings(program, 1, varyings, GL_SEPARATE_ATTRIBS);
|
||||
LinkProgram(program);
|
||||
|
||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// The request resolved rather than being quietly dropped: the interface reports it back.
|
||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 1);
|
||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE), GL_SEPARATE_ATTRIBS);
|
||||
|
||||
GLchar name[128] = {'\0'};
|
||||
GLsizei length = 0;
|
||||
GLsizei size = 0;
|
||||
GLenum type = 0;
|
||||
GetTransformFeedbackVarying(program, 0, sizeof(name), &length, &size, &type, name);
|
||||
EXPECT_EQ(std::string(name, name + (length < 0 ? 0 : length)), "BLOCK_INOUT.value");
|
||||
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// Control: the same program with no capture request. It linked before the fix too, which is
|
||||
// what keeps this case honest about WHICH half of the link moved.
|
||||
TEST_F(TessellationLinkTest, TcsOnlySeparableProgramWithoutXfbVaryingsLinks) {
|
||||
const GLuint program = MakeProgram({{GL_TESS_CONTROL_SHADER, kCtsTessControl}}, /*separable=*/true);
|
||||
LinkProgram(program);
|
||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 0);
|
||||
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// A program with no vertex-processing stage at all still has to be refused - the fix widened
|
||||
// the search, it did not remove the check.
|
||||
TEST_F(TessellationLinkTest, FragmentOnlySeparableProgramWithXfbVaryingsStillFailsToLink) {
|
||||
constexpr const char* fs = R"(#version 460 core
|
||||
out vec4 color;
|
||||
void main() { color = vec4(1.0); }
|
||||
)";
|
||||
const GLuint program = MakeProgram({{GL_FRAGMENT_SHADER, fs}}, /*separable=*/true);
|
||||
const GLchar* const varyings[1] = {"color"};
|
||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
LinkProgram(program);
|
||||
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
|
||||
EXPECT_NE(LinkLog(program).find("no vertex-processing stage"), std::string::npos) << LinkLog(program);
|
||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
constexpr const char* kPassthroughVs = R"(#version 460 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kTcsWithPatchScalar = R"(#version 460 core
|
||||
layout (vertices = 3) out;
|
||||
patch out vec4 tcs_patch;
|
||||
out vec4 tcs_per_vertex[];
|
||||
void main() {
|
||||
tcs_patch = vec4(1.0);
|
||||
tcs_per_vertex[gl_InvocationID] = vec4(2.0);
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kTesWithPatchScalar = R"(#version 460 core
|
||||
layout (triangles) in;
|
||||
patch in vec4 tcs_patch;
|
||||
in vec4 tcs_per_vertex[];
|
||||
out vec4 tes_out;
|
||||
void main() {
|
||||
tes_out = tcs_patch + tcs_per_vertex[0];
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
// The capture stage of a COMPLETE pipeline is unchanged by the widened search: tessellation
|
||||
// control sits AFTER tessellation evaluation in the order, so a program that has both still
|
||||
// resolves its capture names against the EVALUATION stage's outputs. Both halves are pinned -
|
||||
// an evaluation output resolves, a control output does not.
|
||||
TEST_F(TessellationLinkTest, CompletePipelineStillCapturesAtTheEvaluationStage) {
|
||||
{
|
||||
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
|
||||
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
|
||||
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
|
||||
/*separable=*/true);
|
||||
const GLchar* const varyings[1] = {"tes_out"};
|
||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
LinkProgram(program);
|
||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 1);
|
||||
}
|
||||
{
|
||||
// tcs_per_vertex is an output of the CONTROL stage, which is not the capture stage
|
||||
// here. Resolving it would mean capturing at the wrong stage, so the link must fail.
|
||||
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
|
||||
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
|
||||
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
|
||||
/*separable=*/true);
|
||||
const GLchar* const varyings[1] = {"tcs_per_vertex"};
|
||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
||||
LinkProgram(program);
|
||||
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE) << LinkLog(program);
|
||||
}
|
||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// A patch-qualified SCALAR crosses the TCS/TES boundary today. It is the control for the
|
||||
// array case below: same qualifier, same stages, only the arrayness differs.
|
||||
TEST_F(TessellationLinkTest, PatchQualifiedScalarLinksAcrossTheTessellationStages) {
|
||||
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
|
||||
{GL_TESS_CONTROL_SHADER, kTcsWithPatchScalar},
|
||||
{GL_TESS_EVALUATION_SHADER, kTesWithPatchScalar}},
|
||||
/*separable=*/true);
|
||||
LinkProgram(program);
|
||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
||||
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// `patch out int a[N]` against `patch in int a[N]`: legal GLSL, identical spellings, and until
|
||||
// the glslang fork was re-pinned at d89cf443 refused with "Array sizes must be compatible"
|
||||
// while printing the two sides as the same type. That is what failed
|
||||
// KHR-GL4x.tessellation_shader.tessellation_shader_tc_barriers.* on all three API versions.
|
||||
//
|
||||
// The defect was one asymmetric clause in glslang, never in MobileGL:
|
||||
// 3rdparty/glslang/glslang/MachineIndependent/linkValidate.cpp, TIntermediate::isIoResizeArray.
|
||||
// The TessControl arm is guarded with `&& ! type.getQualifier().patch`; the TessEvaluation arm
|
||||
// was not. A patch-qualified array therefore answered false on the control side and true on the
|
||||
// evaluation side, and the caller's dimension arithmetic (linkValidate.cpp:1200-1218) computed
|
||||
// (numDim - firstDim) == (unitNumDim - unitFirstDim) as (1 - 0) == (1 - 1), i.e. false. The
|
||||
// fork now mirrors the control arm, so both sides answer false, the comparison falls to
|
||||
// sameArrayness, and identical int[16] declarations match.
|
||||
//
|
||||
// A hard assertion, with no escape hatch: this case carried a message-matched GTEST_SKIP while
|
||||
// the fix was outstanding, and leaving it in after the pin moved would turn a rolled-back fork
|
||||
// into a silent skip instead of the failure it should be.
|
||||
TEST_F(TessellationLinkTest, PatchQualifiedArrayLinksAcrossTheTessellationStages) {
|
||||
constexpr const char* tcs = R"(#version 460 core
|
||||
layout (vertices = 3) out;
|
||||
patch out int tcs_patch_result[16];
|
||||
void main() {
|
||||
for (int i = 0; i < 16; ++i) { tcs_patch_result[i] = i; }
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
constexpr const char* tes = R"(#version 460 core
|
||||
layout (triangles) in;
|
||||
patch in int tcs_patch_result[16];
|
||||
out vec4 tes_out;
|
||||
void main() {
|
||||
tes_out = vec4(float(tcs_patch_result[0] + tcs_patch_result[15]));
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
const GLuint program = MakeProgram({{GL_VERTEX_SHADER, kPassthroughVs},
|
||||
{GL_TESS_CONTROL_SHADER, tcs},
|
||||
{GL_TESS_EVALUATION_SHADER, tes}},
|
||||
/*separable=*/true);
|
||||
LinkProgram(program);
|
||||
const std::string log = LinkLog(program);
|
||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE)
|
||||
<< "a patch-qualified array must cross the TCS/TES boundary; if this says \"Array sizes "
|
||||
"must be compatible\" the glslang fork pin has lost the isIoResizeArray patch guard.\n"
|
||||
<< log;
|
||||
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
} // namespace
|
||||
@@ -827,10 +827,11 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) {
|
||||
// The NINE unconditional targets. The two tessellation ones are conditional on tessellation
|
||||
// support and have their own test below.
|
||||
static constexpr GLenum kTargets[] = {
|
||||
GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED,
|
||||
GL_VERTEX_SHADER_INVOCATIONS, GL_TESS_CONTROL_SHADER_PATCHES,
|
||||
GL_TESS_EVALUATION_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
|
||||
GL_VERTEX_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
|
||||
GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS,
|
||||
GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES,
|
||||
GL_CLIPPING_OUTPUT_PRIMITIVES,
|
||||
@@ -872,6 +873,73 @@ TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits)
|
||||
}
|
||||
}
|
||||
|
||||
// GL_TESS_CONTROL_SHADER_PATCHES / GL_TESS_EVALUATION_SHADER_INVOCATIONS are the two
|
||||
// pipeline-statistics targets ARB_pipeline_statistics_query makes CONDITIONAL on tessellation
|
||||
// support, and the extension string is the only thing an application can read to decide whether
|
||||
// an implementation has it. So the target and the string have to move together: accepting a
|
||||
// tessellation-conditional token while withholding the string that announces the condition is
|
||||
// self-contradictory, and the conformance suite catches exactly that contradiction
|
||||
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls demands
|
||||
// GL_INVALID_ENUM for every target its own probe calls unsupported, and its probe for these two
|
||||
// is `compatibility(4,0) || GL_ARB_tessellation_shader` - a CORE context fails the first half).
|
||||
//
|
||||
// Written against the advertisement rather than against today's answer on purpose: the day a
|
||||
// backend starts emitting GL_ARB_tessellation_shader this test keeps passing and keeps pinning
|
||||
// the coupling, and it fails loudly if only one of the two halves moves.
|
||||
TEST_F(QueryTest, TessellationPipelineStatisticsTargetsFollowTheTessellationShaderAdvertisement) {
|
||||
const auto* extensionsString =
|
||||
reinterpret_cast<const char*>(MG_Impl::GLImpl::GetString(GL_EXTENSIONS));
|
||||
ASSERT_NE(extensionsString, nullptr);
|
||||
const Bool advertised = String(extensionsString).find("GL_ARB_tessellation_shader") != String::npos;
|
||||
const GLenum expectedError = advertised ? GL_NO_ERROR : GL_INVALID_ENUM;
|
||||
|
||||
static constexpr GLenum kTessTargets[] = {
|
||||
GL_TESS_CONTROL_SHADER_PATCHES,
|
||||
GL_TESS_EVALUATION_SHADER_INVOCATIONS,
|
||||
};
|
||||
|
||||
for (const GLenum target: kTessTargets) {
|
||||
GLuint id = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||
ASSERT_NE(id, 0u);
|
||||
|
||||
MG_Impl::GLImpl::BeginQuery(target, id);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expectedError)
|
||||
<< "glBeginQuery on tessellation pipeline-statistics target 0x" << std::hex << target
|
||||
<< " must agree with the GL_ARB_tessellation_shader advertisement";
|
||||
|
||||
if (advertised) {
|
||||
MG_Impl::GLImpl::EndQuery(target);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
GLint counterBits = -1;
|
||||
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(counterBits, 0);
|
||||
} else {
|
||||
// Refused at glEndQuery too, not just at glBeginQuery: a target the implementation
|
||||
// does not have is not half-accepted.
|
||||
MG_Impl::GLImpl::EndQuery(target);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
|
||||
// GL_QUERY_COUNTER_BITS still answers the honest zero rather than an error - the
|
||||
// getter has never validated its target, and zero is what "no such counter" reads as
|
||||
// (GL 4.6 core 4.2.1), so the refusal costs no information.
|
||||
GLint counterBits = -1;
|
||||
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(counterBits, 0);
|
||||
// And GL_CURRENT_QUERY reads as "no query" rather than tracking a slot that was
|
||||
// never opened.
|
||||
GLint current = -1;
|
||||
MG_Impl::GLImpl::GetQueryiv(target, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(current, 0);
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The negative half the conformance case actually asserts: an object already latched onto one
|
||||
// pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what
|
||||
// per-target active slots buy - a single shared slot would have reported "a query is already
|
||||
|
||||
@@ -18,6 +18,7 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
|
||||
add_executable(
|
||||
DriverBugProbesTest
|
||||
DriverBugProbesTest.cpp
|
||||
PersistentBufferOrderingProbeTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DriverBugProbesTest PRIVATE
|
||||
@@ -30,6 +31,22 @@ target_link_libraries(DriverBugProbesTest PRIVATE
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_executable(
|
||||
PrimitivesGeneratedNoXfbProbeTest
|
||||
PrimitivesGeneratedNoXfbProbeTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PrimitivesGeneratedNoXfbProbeTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(PrimitivesGeneratedNoXfbProbeTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(PrimitivesGeneratedNoXfbProbeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -26,6 +26,7 @@ using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeCopyImageMirrorsPacked16FieldOrder;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||
|
||||
@@ -107,6 +108,28 @@ namespace {
|
||||
bool blitIgnoresDestinationLayer = false;
|
||||
bool blitIgnoresSourceLayer = false;
|
||||
|
||||
// Probe 7: the driver stores a WHOLE 16-bit packed ALLOCATION with its fields packed
|
||||
// from the other end of the word - on the measured device, every level of the probe's
|
||||
// 30x30x12 three-level array, while same-shape plain-2D images stay in the canonical
|
||||
// order. Modelled at the raw copy, which is the only path that can observe it (uploads
|
||||
// and readbacks of the same image decode the driver's own layout consistently): a copy
|
||||
// whose SOURCE is any level of the mirrored allocation delivers the mirrored
|
||||
// re-encoding, which the plain-2D readback then decodes with the non-REV order -
|
||||
// exactly the 0x0047 -> 0x8C20 arithmetic the affected Mali hands back. The mirror
|
||||
// only engages for the allocation the failures were measured on - a THREE-level
|
||||
// 30x30x12 array - so a probe that stopped building the triggering shape (fewer
|
||||
// levels, other dimensions) stops detecting, which is exactly what these tests are
|
||||
// for.
|
||||
bool packed16ArrayAllocationMirrored = false;
|
||||
// "Not this bug": the UPLOAD corrupts, so the array's own direct readback is already
|
||||
// wrong. The probe's round-trip control must veto the verdict - the widening's
|
||||
// raw-copy reasoning says nothing about an upload defect.
|
||||
bool packed16UploadCorrupted = false;
|
||||
// The inconclusive path: the copy silently lands nothing, so every destination keeps
|
||||
// its 0xFFFF fill - a value that is neither the word nor its mirror - and the 2D-to-2D
|
||||
// machinery control fails first.
|
||||
bool packed16CopyDoesNothing = false;
|
||||
|
||||
// ---- object bookkeeping ---------------------------------------------
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
GLuint nextShaderId = 1;
|
||||
@@ -136,7 +159,34 @@ namespace {
|
||||
std::map<GLuint, std::array<GLubyte, 2>> arrayLayerFill;
|
||||
// framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached.
|
||||
std::map<GLuint, std::pair<GLuint, GLint>> framebufferLayerAttachment;
|
||||
// framebuffer id -> the LEVEL that same call named. Kept apart so the layered-blit
|
||||
// bookkeeping above keeps its shape; the packed16 probe reads array LEVELS directly.
|
||||
std::map<GLuint, GLint> framebufferLayerLevel;
|
||||
// (texture, level) -> the PHYSICAL 16-bit word every texel of that 5551 image holds.
|
||||
// One word per level is all the packed16 probe distinguishes: it uploads a uniform
|
||||
// fill and reads one texel.
|
||||
std::map<std::pair<GLuint, GLint>, GLushort> packedTexelWords;
|
||||
// 2D-array texture id -> its allocation shape, as glTexImage3D built it. What the
|
||||
// packed16 mirror is gated on: level-0 dimensions plus a mask of the levels actually
|
||||
// allocated, so only the measured three-level 30x30x12 chain diverges.
|
||||
struct FakeArrayAllocation {
|
||||
GLsizei width = 0;
|
||||
GLsizei height = 0;
|
||||
GLsizei layers = 0;
|
||||
unsigned levelMask = 0;
|
||||
// The device rule the probe reproduces: the mirrored layout is only picked when
|
||||
// the levels were uploaded onto a texture still at the driver defaults - any
|
||||
// glTexParameteri BEFORE the first upload steers the driver to the plain layout.
|
||||
// Modelling it makes a params-first probe (the round-one regression: it measured
|
||||
// "clean" in the very context whose params-after textures mirrored) stop
|
||||
// detecting, which turns that mistake into a red test instead of a silent miss.
|
||||
bool paramsTouchedBeforeUpload = false;
|
||||
};
|
||||
std::map<GLuint, FakeArrayAllocation> packedArrayAllocations;
|
||||
// framebuffer id -> the plain 2D texture glFramebufferTexture2D attached.
|
||||
std::map<GLuint, GLuint> framebuffer2DAttachment;
|
||||
GLuint boundArrayTexture = 0;
|
||||
GLuint boundTexture2D = 0;
|
||||
GLuint boundDrawFramebuffer = 0;
|
||||
GLuint boundReadFramebuffer = 0;
|
||||
|
||||
@@ -164,6 +214,17 @@ namespace {
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
// The 5_5_5_1 <-> 1_5_5_5_REV field-order mirror: the same fields, packed from the other
|
||||
// end of the word. 0x0047 (R,G,B,A = 0,1,3,1) becomes 0x8C20 - the exact pair every
|
||||
// failing KHR-GL4x.copy_image body printed on the affected Mali.
|
||||
GLushort MirrorPacked5551(GLushort word) {
|
||||
const GLushort r = (word >> 11) & 0x1F;
|
||||
const GLushort g = (word >> 6) & 0x1F;
|
||||
const GLushort b = (word >> 1) & 0x1F;
|
||||
const GLushort a = word & 0x1;
|
||||
return static_cast<GLushort>((a << 15) | (b << 10) | (g << 5) | r);
|
||||
}
|
||||
|
||||
// Every `image2D <name>` the program declares, across all its stages.
|
||||
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
||||
std::vector<std::string> names;
|
||||
@@ -401,6 +462,7 @@ namespace {
|
||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.boundArrayTexture = texture;
|
||||
if (target == GL_TEXTURE_2D) g_fake.boundTexture2D = texture;
|
||||
};
|
||||
funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) {
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.arrayLayerFill[g_fake.boundArrayTexture] = {0, 0};
|
||||
@@ -418,6 +480,14 @@ namespace {
|
||||
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (textures[i] != 0) --g_fake.aliveTextures;
|
||||
for (auto it = g_fake.packedTexelWords.begin(); it != g_fake.packedTexelWords.end();) {
|
||||
if (it->first.first == textures[i]) {
|
||||
it = g_fake.packedTexelWords.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
g_fake.packedArrayAllocations.erase(textures[i]);
|
||||
}
|
||||
};
|
||||
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
|
||||
@@ -425,9 +495,63 @@ namespace {
|
||||
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
|
||||
static_cast<GLenum>(param);
|
||||
}
|
||||
// A parameter write on a 2D array that has no uploaded level yet steers the
|
||||
// driver's layout choice to the plain order (see FakeArrayAllocation).
|
||||
if (target == GL_TEXTURE_2D_ARRAY &&
|
||||
g_fake.packedArrayAllocations.count(g_fake.boundArrayTexture) == 0) {
|
||||
g_fake.packedArrayAllocations[g_fake.boundArrayTexture].paramsTouchedBeforeUpload = true;
|
||||
}
|
||||
};
|
||||
// The packed16 probe's endpoints. A plain 2D image stores its 5551 words in the
|
||||
// canonical (non-REV) order on every knob setting - the defect is confined to array
|
||||
// mip levels, and keeping the 2D side clean is what lets the readback below decode
|
||||
// with one order and still reproduce the mirror.
|
||||
funcs.glTexImage2D = [](GLenum target, GLint level, GLint, GLsizei, GLsizei, GLint, GLenum,
|
||||
GLenum type, const void* pixels) {
|
||||
if (target != GL_TEXTURE_2D || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return;
|
||||
GLushort word = 0;
|
||||
std::memcpy(&word, pixels, sizeof(word));
|
||||
g_fake.packedTexelWords[{g_fake.boundTexture2D, level}] = word;
|
||||
};
|
||||
// Records the allocation shape the mirror below is gated on, and the uploaded word.
|
||||
// Under the upload-corruption knob the STORED word is already wrong - the "not this
|
||||
// bug" shape the probe's round-trip control must catch.
|
||||
funcs.glTexImage3D = [](GLenum target, GLint level, GLint, GLsizei width, GLsizei height,
|
||||
GLsizei depth, GLint, GLenum, GLenum type, const void* pixels) {
|
||||
if (target != GL_TEXTURE_2D_ARRAY || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return;
|
||||
GLushort word = 0;
|
||||
std::memcpy(&word, pixels, sizeof(word));
|
||||
g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] =
|
||||
g_fake.packed16UploadCorrupted ? MirrorPacked5551(word) : word;
|
||||
auto& allocation = g_fake.packedArrayAllocations[g_fake.boundArrayTexture];
|
||||
if (level == 0) {
|
||||
allocation.width = width;
|
||||
allocation.height = height;
|
||||
allocation.layers = depth;
|
||||
}
|
||||
if (level >= 0 && level < 8) allocation.levelMask |= 1u << level;
|
||||
};
|
||||
// A raw texel-block move: the PHYSICAL word travels. The defect lives here - a source
|
||||
// in the mirrored ALLOCATION delivers the re-encoded word from EVERY level - and it
|
||||
// only exists for the allocation it was measured on: three levels of a 30x30x12 array.
|
||||
funcs.glCopyImageSubData = [](GLuint srcName, GLenum, GLint srcLevel, GLint, GLint, GLint,
|
||||
GLuint dstName, GLenum, GLint dstLevel, GLint, GLint, GLint,
|
||||
GLsizei, GLsizei, GLsizei) {
|
||||
if (g_fake.packed16CopyDoesNothing) return;
|
||||
const auto source = g_fake.packedTexelWords.find({srcName, srcLevel});
|
||||
if (source == g_fake.packedTexelWords.end()) return;
|
||||
GLushort word = source->second;
|
||||
const auto allocation = g_fake.packedArrayAllocations.find(srcName);
|
||||
const bool measuredShape = allocation != g_fake.packedArrayAllocations.end() &&
|
||||
allocation->second.width == 30 && allocation->second.height == 30 &&
|
||||
allocation->second.layers == 12 &&
|
||||
allocation->second.levelMask == 0b111u &&
|
||||
!allocation->second.paramsTouchedBeforeUpload;
|
||||
if (measuredShape && g_fake.packed16ArrayAllocationMirrored) {
|
||||
word = MirrorPacked5551(word);
|
||||
}
|
||||
g_fake.packedTexelWords[{dstName, dstLevel}] = word;
|
||||
};
|
||||
funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
|
||||
const void*) {};
|
||||
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||
const void*) {};
|
||||
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||
@@ -446,11 +570,16 @@ namespace {
|
||||
g_fake.boundReadFramebuffer = framebuffer;
|
||||
}
|
||||
};
|
||||
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||
funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) {
|
||||
funcs.glFramebufferTexture2D = [](GLenum target, GLenum, GLenum, GLuint texture, GLint) {
|
||||
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||
: g_fake.boundDrawFramebuffer;
|
||||
g_fake.framebuffer2DAttachment[framebuffer] = texture;
|
||||
};
|
||||
funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint level, GLint layer) {
|
||||
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||
: g_fake.boundDrawFramebuffer;
|
||||
g_fake.framebufferLayerAttachment[framebuffer] = {texture, layer};
|
||||
g_fake.framebufferLayerLevel[framebuffer] = level;
|
||||
};
|
||||
funcs.glReadBuffer = [](GLenum) {};
|
||||
// The defect itself: the source layer is read from where the READ framebuffer says (unless
|
||||
@@ -482,6 +611,8 @@ namespace {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
||||
g_fake.framebufferLayerLevel.erase(framebuffers[i]);
|
||||
g_fake.framebuffer2DAttachment.erase(framebuffers[i]);
|
||||
}
|
||||
};
|
||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||
@@ -561,11 +692,54 @@ namespace {
|
||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
// Answered before anything else: a read framebuffer that names an array LAYER is the
|
||||
// layered-blit probe asking what that layer holds, and its bytes have nothing to do
|
||||
// with the pass/fail texel encoding the image probes below share.
|
||||
// A read framebuffer naming a plain 2D texture that holds a 5551 word is the
|
||||
// packed16 probe reading its copy destination. The driver decodes its OWN storage
|
||||
// with the canonical non-REV order and expands each field by bit replication -
|
||||
// which is exactly how the mirrored word 0x8C20 becomes (140, 132, 132, 0).
|
||||
if (const auto attached = g_fake.framebuffer2DAttachment.find(g_fake.boundReadFramebuffer);
|
||||
attached != g_fake.framebuffer2DAttachment.end() &&
|
||||
g_fake.packedTexelWords.count({attached->second, 0}) != 0) {
|
||||
// Gated on the texture actually holding a 5551 word, so every OTHER probe that
|
||||
// attaches a plain 2D texture keeps the pass/fail readback paths below.
|
||||
const GLushort w = g_fake.packedTexelWords[{attached->second, 0}];
|
||||
const auto expand5 = [](GLushort v) {
|
||||
return static_cast<GLubyte>((v << 3) | (v >> 2));
|
||||
};
|
||||
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
out[i * 4 + 0] = expand5((w >> 11) & 0x1F);
|
||||
out[i * 4 + 1] = expand5((w >> 6) & 0x1F);
|
||||
out[i * 4 + 2] = expand5((w >> 1) & 0x1F);
|
||||
out[i * 4 + 3] = (w & 0x1) ? 255 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A read framebuffer naming an array LEVEL that holds a 5551 word is the packed16
|
||||
// probe's round-trip control: the driver decodes its OWN storage, so whatever the
|
||||
// physical word is - mirrored at upload under that knob included - its own decode
|
||||
// is handed back with the canonical field meaning.
|
||||
if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer);
|
||||
layered != g_fake.framebufferLayerAttachment.end()) {
|
||||
const auto levelIt = g_fake.framebufferLayerLevel.find(g_fake.boundReadFramebuffer);
|
||||
const GLint attachedLevel = levelIt == g_fake.framebufferLayerLevel.end() ? 0 : levelIt->second;
|
||||
if (const auto word = g_fake.packedTexelWords.find({layered->second.first, attachedLevel});
|
||||
word != g_fake.packedTexelWords.end()) {
|
||||
const GLushort w = word->second;
|
||||
const auto expand5 = [](GLushort v) {
|
||||
return static_cast<GLubyte>((v << 3) | (v >> 2));
|
||||
};
|
||||
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
out[i * 4 + 0] = expand5((w >> 11) & 0x1F);
|
||||
out[i * 4 + 1] = expand5((w >> 6) & 0x1F);
|
||||
out[i * 4 + 2] = expand5((w >> 1) & 0x1F);
|
||||
out[i * 4 + 3] = (w & 0x1) ? 255 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Otherwise it is the layered-blit probe asking what a layer holds, and its
|
||||
// bytes have nothing to do with the pass/fail texel encoding the image probes
|
||||
// below share.
|
||||
const auto& fill = g_fake.arrayLayerFill[layered->second.first];
|
||||
const GLint layer = layered->second.second;
|
||||
const GLubyte value =
|
||||
@@ -626,6 +800,8 @@ TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl))
|
||||
<< "a probe with no entry points has measured nothing";
|
||||
}
|
||||
|
||||
// The section lists only bugs the device HAS, so a driver nothing could be probed on renders
|
||||
@@ -945,3 +1121,53 @@ TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, Packed16FieldOrderIsCleanOnAConformingDriver) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The measured device shape: EVERY level of the mirrored allocation delivers the
|
||||
// re-encoding, and the machinery/round-trip controls stay clean, so the probe must detect.
|
||||
TEST(DriverBugProbes, Packed16FieldOrderIsDetectedWhenTheArrayAllocationIsMirrored) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16ArrayAllocationMirrored = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_TRUE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// THE ROUND-TRIP CONTROL. A driver that corrupts the UPLOAD hands the mirror back from the
|
||||
// array's own direct readback too - a different defect, and one the widening's raw-copy
|
||||
// reasoning says nothing about - so the probe must reach no verdict rather than claim it.
|
||||
TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheUploadItselfCorrupts) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16UploadCorrupted = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// And the shape that is not this bug: a copy that lands nothing leaves every destination's
|
||||
// 0xFFFF fill, so the 2D-to-2D machinery control fails first - "reached no verdict".
|
||||
TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheCopyLandsNothing) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16ArrayAllocationMirrored = true;
|
||||
g_fake.packed16CopyDoesNothing = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The byte arithmetic the fake's mirror encodes, pinned against the QPA evidence. The fake
|
||||
// models the ARRAY-AS-SOURCE direction (decode 5_5_5_1, re-encode 1_5_5_5_REV): 0x0047 must
|
||||
// deliver 0x8C20, the exact pair every failing array-as-source copy_image body printed. The
|
||||
// QPA's array-as-destination bodies show the INVERSE transform (enc_5551 of dec_REV: 0x0007
|
||||
// delivered as 0x3800), and enc_REV(dec_5551(x)) inverts enc_5551(dec_REV(x)), so feeding
|
||||
// the delivered word back through the fake's mirror must reproduce the original.
|
||||
TEST(DriverBugProbes, Packed16MirrorArithmeticMatchesTheDeviceEvidence) {
|
||||
EXPECT_EQ(MirrorPacked5551(0x0047), 0x8C20);
|
||||
EXPECT_EQ(MirrorPacked5551(0x3800), 0x0007);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
// MobileGL - MobileGL/MG_Test/SelfTest/PersistentBufferOrderingProbeTest.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <MG_Util/SelfTest/PersistentBufferOrderingProbe.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Util::SelfTest;
|
||||
|
||||
namespace {
|
||||
// Deferred vertex fetch, not canned ReadPixels answers: a broken mapped destination
|
||||
// reads its current bytes at Finish instead of the bytes at DrawArrays. ReadPixels also
|
||||
// drains these jobs, so inserting an early readback into the probe hides the bug here too.
|
||||
struct FakeDriver {
|
||||
struct Buffer {
|
||||
Bool mapped = false;
|
||||
Bool arena = false;
|
||||
Bool copied = false;
|
||||
Int channel = 0;
|
||||
Vector<Uint8> staging;
|
||||
};
|
||||
struct Draw { GLuint fbo, buffer; Int channel; Bool late; };
|
||||
std::map<GLuint, Buffer> buffers;
|
||||
std::map<GLuint, GLuint> vaoBuffers;
|
||||
std::map<GLuint, Int> colors;
|
||||
Vector<Draw> draws;
|
||||
std::set<GLuint> live;
|
||||
std::map<GLenum, GLint> state = {
|
||||
{GL_CURRENT_PROGRAM, 1}, {GL_VERTEX_ARRAY_BINDING, 2}, {GL_ARRAY_BUFFER, 3},
|
||||
{GL_COPY_READ_BUFFER, 4}, {GL_COPY_WRITE_BUFFER, 5},
|
||||
{GL_DRAW_FRAMEBUFFER_BINDING, 6}, {GL_READ_FRAMEBUFFER_BINDING, 7},
|
||||
{GL_TEXTURE_BINDING_2D, 8}, {GL_PIXEL_PACK_BUFFER, 9},
|
||||
{GL_PACK_ALIGNMENT, 8}, {GL_PACK_ROW_LENGTH, 31},
|
||||
{GL_PACK_SKIP_PIXELS, 4}, {GL_PACK_SKIP_ROWS, 5}};
|
||||
std::map<GLenum, GLboolean> enabled = {{GL_BLEND, GL_TRUE}, {GL_SCISSOR_TEST, GL_TRUE},
|
||||
{GL_SAMPLE_MASK, GL_TRUE}, {GL_RASTERIZER_DISCARD, GL_TRUE}};
|
||||
std::array<GLint, 4> viewport = {3, 4, 5, 6};
|
||||
std::array<GLfloat, 4> clear = {.25f, .5f, .75f, 0};
|
||||
std::array<GLboolean, 4> mask = {GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE};
|
||||
GLuint next = 100;
|
||||
GLenum error = GL_NO_ERROR;
|
||||
Bool extension = true, corruptSubData = false, corruptCopy = false;
|
||||
Bool corruptUnmapped = false, corruptSerialized = false;
|
||||
Bool failMap = false, failAllocation = false, failFramebuffer = false;
|
||||
Int failReadbackAt = 0, readbacks = 0;
|
||||
Uint arenaAllocations = 0;
|
||||
Uint8 pointerSentinel = 0;
|
||||
|
||||
GLuint Create() { live.insert(next); return next++; }
|
||||
void Generate(GLsizei count, GLuint* ids) { for (Int i = 0; i < count; ++i) ids[i] = Create(); }
|
||||
void Delete(GLsizei count, const GLuint* ids) {
|
||||
for (Int i = 0; i < count; ++i) {
|
||||
live.erase(ids[i]);
|
||||
buffers.erase(ids[i]);
|
||||
}
|
||||
}
|
||||
GLint Get(GLenum name) const {
|
||||
const auto found = state.find(name);
|
||||
return found == state.end() ? 0 : found->second;
|
||||
}
|
||||
static Int Channel(const void* data) {
|
||||
GLfloat color[3];
|
||||
std::memcpy(color, static_cast<const Uint8*>(data) + 2 * sizeof(GLfloat), sizeof(color));
|
||||
return color[0] > .5f ? 0 : color[1] > .5f ? 1 : 2;
|
||||
}
|
||||
void Finish() {
|
||||
for (const auto& draw : draws) {
|
||||
const auto& buffer = buffers.at(draw.buffer);
|
||||
colors[draw.fbo] = draw.late ? buffer.channel : draw.channel;
|
||||
if (buffer.mapped && corruptSerialized) colors[draw.fbo] = (draw.channel + 1) % 3;
|
||||
}
|
||||
draws.clear();
|
||||
}
|
||||
} driver;
|
||||
|
||||
MG_External::GLESFunctionsTable Table() {
|
||||
MG_External::GLESFunctionsTable gl{};
|
||||
gl.glGetIntegerv = [](GLenum name, GLint* out) {
|
||||
if (name == GL_MAJOR_VERSION) *out = 3;
|
||||
else if (name == GL_MINOR_VERSION) *out = 2;
|
||||
else if (name == GL_NUM_EXTENSIONS) *out = driver.extension ? 1 : 0;
|
||||
else if (name == GL_VIEWPORT) std::copy(driver.viewport.begin(), driver.viewport.end(), out);
|
||||
else if (name == GL_ARRAY_BUFFER_BINDING) *out = driver.Get(GL_ARRAY_BUFFER);
|
||||
else if (name == GL_PIXEL_PACK_BUFFER_BINDING) *out = driver.Get(GL_PIXEL_PACK_BUFFER);
|
||||
else *out = driver.Get(name);
|
||||
};
|
||||
gl.glGetBooleanv = [](GLenum, GLboolean* out) { std::copy(driver.mask.begin(), driver.mask.end(), out); };
|
||||
gl.glGetFloatv = [](GLenum, GLfloat* out) { std::copy(driver.clear.begin(), driver.clear.end(), out); };
|
||||
gl.glGetStringi = [](GLenum, GLuint) { return reinterpret_cast<const GLubyte*>("GL_EXT_buffer_storage"); };
|
||||
gl.glGetError = []() { return std::exchange(driver.error, GL_NO_ERROR); };
|
||||
gl.glIsEnabled = [](GLenum name) -> GLboolean { return driver.enabled[name]; };
|
||||
gl.glEnable = [](GLenum name) { driver.enabled[name] = GL_TRUE; };
|
||||
gl.glDisable = [](GLenum name) { driver.enabled[name] = GL_FALSE; };
|
||||
gl.glCreateShader = [](GLenum) { return driver.Create(); };
|
||||
gl.glShaderSource = [](GLuint, GLsizei, const GLchar* const*, const GLint*) {};
|
||||
gl.glCompileShader = [](GLuint) {};
|
||||
gl.glGetShaderiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; };
|
||||
gl.glGetShaderInfoLog = [](GLuint, GLsizei, GLsizei*, GLchar* out) { *out = 0; };
|
||||
gl.glDeleteShader = [](GLuint id) { driver.Delete(1, &id); };
|
||||
gl.glCreateProgram = []() { return driver.Create(); };
|
||||
gl.glAttachShader = [](GLuint, GLuint) {};
|
||||
gl.glLinkProgram = [](GLuint) {};
|
||||
gl.glGetProgramiv = [](GLuint, GLenum, GLint* out) { *out = GL_TRUE; };
|
||||
gl.glGetProgramInfoLog = gl.glGetShaderInfoLog;
|
||||
gl.glDeleteProgram = gl.glDeleteShader;
|
||||
gl.glUseProgram = [](GLuint id) { driver.state[GL_CURRENT_PROGRAM] = id; };
|
||||
gl.glGenBuffers = [](GLsizei count, GLuint* ids) { driver.Generate(count, ids); };
|
||||
gl.glBindBuffer = [](GLenum target, GLuint id) { driver.state[target] = id; };
|
||||
gl.glBufferStorageEXT = [](GLenum target, GLsizeiptr size, const void*, GLbitfield) {
|
||||
auto& buffer = driver.buffers[driver.Get(target)];
|
||||
buffer.arena = size >= 16 * 1024 * 1024;
|
||||
if (buffer.arena) ++driver.arenaAllocations;
|
||||
if (driver.failAllocation) driver.error = GL_OUT_OF_MEMORY;
|
||||
if (!buffer.arena) buffer.staging.resize(size);
|
||||
};
|
||||
gl.glBufferData = [](GLenum target, GLsizeiptr size, const void*, GLenum) {
|
||||
driver.buffers[driver.Get(target)].staging.resize(size);
|
||||
};
|
||||
gl.glMapBufferRange = [](GLenum target, GLintptr offset, GLsizeiptr, GLbitfield) -> void* {
|
||||
if (driver.failMap) return nullptr;
|
||||
auto& buffer = driver.buffers[driver.Get(target)];
|
||||
buffer.mapped = true;
|
||||
return buffer.arena ? &driver.pointerSentinel : buffer.staging.data() + offset;
|
||||
};
|
||||
gl.glUnmapBuffer = [](GLenum) -> GLboolean { return GL_TRUE; }; // Preserve allocation history.
|
||||
gl.glBufferSubData = [](GLenum target, GLintptr offset, GLsizeiptr size, const void* data) {
|
||||
auto& buffer = driver.buffers[driver.Get(target)];
|
||||
if (buffer.arena) {
|
||||
buffer.channel = FakeDriver::Channel(data);
|
||||
buffer.copied = false;
|
||||
} else std::memcpy(buffer.staging.data() + offset, data, size);
|
||||
};
|
||||
gl.glCopyBufferSubData = [](GLenum read, GLenum write, GLintptr offset, GLintptr, GLsizeiptr) {
|
||||
auto& source = driver.buffers[driver.Get(read)];
|
||||
auto& dest = driver.buffers[driver.Get(write)];
|
||||
dest.channel = FakeDriver::Channel(source.staging.data() + offset);
|
||||
dest.copied = true;
|
||||
};
|
||||
gl.glDeleteBuffers = [](GLsizei count, const GLuint* ids) { driver.Delete(count, ids); };
|
||||
gl.glGenVertexArrays = gl.glGenBuffers;
|
||||
gl.glBindVertexArray = [](GLuint id) { driver.state[GL_VERTEX_ARRAY_BINDING] = id; };
|
||||
gl.glVertexAttribPointer = [](GLuint, GLint, GLenum, GLboolean, GLsizei, const void*) {
|
||||
driver.vaoBuffers[driver.Get(GL_VERTEX_ARRAY_BINDING)] = driver.Get(GL_ARRAY_BUFFER);
|
||||
};
|
||||
gl.glEnableVertexAttribArray = [](GLuint) {};
|
||||
gl.glDeleteVertexArrays = gl.glDeleteBuffers;
|
||||
gl.glGenTextures = gl.glGenBuffers;
|
||||
gl.glBindTexture = [](GLenum, GLuint id) { driver.state[GL_TEXTURE_BINDING_2D] = id; };
|
||||
gl.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||
gl.glDeleteTextures = gl.glDeleteBuffers;
|
||||
gl.glGenFramebuffers = gl.glGenBuffers;
|
||||
gl.glBindFramebuffer = [](GLenum target, GLuint id) {
|
||||
if (target != GL_READ_FRAMEBUFFER) driver.state[GL_DRAW_FRAMEBUFFER_BINDING] = id;
|
||||
if (target != GL_DRAW_FRAMEBUFFER) driver.state[GL_READ_FRAMEBUFFER_BINDING] = id;
|
||||
};
|
||||
gl.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||
gl.glCheckFramebufferStatus = [](GLenum) -> GLenum {
|
||||
return driver.failFramebuffer ? GL_FRAMEBUFFER_UNSUPPORTED : GL_FRAMEBUFFER_COMPLETE;
|
||||
};
|
||||
gl.glDeleteFramebuffers = gl.glDeleteBuffers;
|
||||
gl.glViewport = [](GLint x, GLint y, GLsizei w, GLsizei h) { driver.viewport = {x, y, w, h}; };
|
||||
gl.glColorMask = [](GLboolean r, GLboolean g, GLboolean b, GLboolean a) { driver.mask = {r, g, b, a}; };
|
||||
gl.glClearColor = [](GLfloat r, GLfloat g, GLfloat b, GLfloat a) { driver.clear = {r, g, b, a}; };
|
||||
gl.glClear = [](GLbitfield) {};
|
||||
gl.glDrawArrays = [](GLenum, GLint, GLsizei) {
|
||||
const GLuint id = driver.vaoBuffers.at(driver.Get(GL_VERTEX_ARRAY_BINDING));
|
||||
const auto& buffer = driver.buffers.at(id);
|
||||
const Bool late = buffer.mapped ? (buffer.copied ? driver.corruptCopy : driver.corruptSubData)
|
||||
: driver.corruptUnmapped;
|
||||
driver.draws.push_back({GLuint(driver.Get(GL_DRAW_FRAMEBUFFER_BINDING)), id, buffer.channel, late});
|
||||
};
|
||||
gl.glFinish = []() { driver.Finish(); };
|
||||
gl.glMemoryBarrier = [](GLbitfield) {};
|
||||
gl.glPixelStorei = [](GLenum name, GLint value) { driver.state[name] = value; };
|
||||
gl.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum, GLenum, void* data) {
|
||||
driver.Finish(); // Models the implicit wait that must NOT occur between subject draws.
|
||||
if (++driver.readbacks == driver.failReadbackAt) {
|
||||
driver.error = GL_INVALID_OPERATION;
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(driver.Get(GL_PIXEL_PACK_BUFFER), 0);
|
||||
EXPECT_EQ(driver.Get(GL_PACK_ROW_LENGTH), 0);
|
||||
const Int channel = driver.colors.at(driver.Get(GL_READ_FRAMEBUFFER_BINDING));
|
||||
auto* pixels = static_cast<Uint8*>(data);
|
||||
for (Int i = 0; i < width * height; ++i)
|
||||
for (Int c = 0; c < 4; ++c) pixels[4 * i + c] = c == channel || c == 3 ? 255 : 0;
|
||||
};
|
||||
return gl;
|
||||
}
|
||||
|
||||
class PersistentBufferOrderingProbeTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { driver = FakeDriver{}; }
|
||||
void TearDown() override { EXPECT_TRUE(driver.live.empty()); EXPECT_TRUE(driver.draws.empty()); }
|
||||
};
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, RequiresExtensionAndCompleteDispatchBeforeAllocating) {
|
||||
auto gl = Table();
|
||||
driver.extension = false;
|
||||
EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported);
|
||||
driver.extension = true;
|
||||
gl.glCopyBufferSubData = nullptr;
|
||||
EXPECT_FALSE(ProbePersistentBufferUpdateOrdering(gl).supported);
|
||||
EXPECT_EQ(driver.arenaAllocations, 0u);
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, OrderedDriverPassesAllUploadsAndRestoresCallerState) {
|
||||
const auto saved = driver;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
ASSERT_TRUE(measurement.supported);
|
||||
for (const auto& row : measurement.uploads) {
|
||||
EXPECT_TRUE(row.unmapped.Passed());
|
||||
EXPECT_TRUE(row.mapped.Passed());
|
||||
EXPECT_EQ(row.mapped.frames, 240u); // Three fresh attempts before a negative result.
|
||||
EXPECT_EQ(row.finishBoth.status, BufferOrderingProbeStatus::NotRun);
|
||||
}
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
EXPECT_EQ(driver.state, saved.state);
|
||||
EXPECT_EQ(driver.viewport, saved.viewport);
|
||||
EXPECT_EQ(driver.clear, saved.clear);
|
||||
EXPECT_EQ(driver.mask, saved.mask);
|
||||
for (const auto& [cap, value] : driver.enabled) {
|
||||
const auto found = saved.enabled.find(cap);
|
||||
EXPECT_EQ(value, found == saved.enabled.end() ? GL_FALSE : found->second);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, DeferredMappedSubDataFetchIsDetectedWithPassingControls) {
|
||||
driver.corruptSubData = true;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
EXPECT_TRUE(measurement.uploads[0].Detected());
|
||||
EXPECT_TRUE(measurement.uploads[0].finishBefore.Passed());
|
||||
EXPECT_GT(measurement.uploads[0].mapThenUnmap.badFrames, 0u);
|
||||
EXPECT_GT(measurement.uploads[0].barrierBefore.badFrames, 0u);
|
||||
EXPECT_FALSE(measurement.uploads[1].Detected());
|
||||
EXPECT_FALSE(measurement.uploads[2].Detected());
|
||||
const auto finding = DescribePersistentBufferOrderingBug(measurement);
|
||||
ASSERT_TRUE(finding);
|
||||
EXPECT_EQ(finding->verdict, DriverBugVerdict::Unfixable);
|
||||
EXPECT_NE(finding->detail.find("SubData:"), String::npos);
|
||||
EXPECT_NE(finding->detail.find("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1"), String::npos);
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, DeferredCopyFetchIsDetectedWithBothStagingSources) {
|
||||
driver.corruptCopy = true;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
EXPECT_FALSE(measurement.uploads[0].Detected());
|
||||
EXPECT_TRUE(measurement.uploads[1].Detected());
|
||||
EXPECT_TRUE(measurement.uploads[2].Detected());
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, PostCollectorIncludesTheMeasuredFinding) {
|
||||
driver.corruptSubData = true;
|
||||
const auto findings = CollectGlesKnownDriverBugs(Table());
|
||||
const auto found = std::find_if(findings.begin(), findings.end(), [](const auto& finding) {
|
||||
return finding.name == "Persistent-mapped vertex buffers lose upload/draw ordering";
|
||||
});
|
||||
ASSERT_NE(found, findings.end());
|
||||
EXPECT_NE(found->detail.find("never-mapped 0/80"), String::npos);
|
||||
EXPECT_EQ(found->verdict, DriverBugVerdict::Unfixable);
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, CorruptNeverMappedControlCannotAccusePersistentMapping) {
|
||||
driver.corruptSubData = driver.corruptCopy = driver.corruptUnmapped = true;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
for (const auto& row : measurement.uploads) {
|
||||
EXPECT_GT(row.unmapped.badFrames, 0u);
|
||||
EXPECT_EQ(row.mapped.status, BufferOrderingProbeStatus::NotRun);
|
||||
}
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, CorruptSerializedControlCannotConfirmOrderingDefect) {
|
||||
driver.corruptSubData = driver.corruptCopy = driver.corruptSerialized = true;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
for (const auto& row : measurement.uploads) EXPECT_GT(row.finishBoth.badFrames, 0u);
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, FailedMappingIsInconclusiveAndReleasesResources) {
|
||||
driver.failMap = true;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed);
|
||||
EXPECT_EQ(measurement.uploads[1].unmapped.status, BufferOrderingProbeStatus::Failed);
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, AllocationFailureIsInconclusiveAndRestoresBindings) {
|
||||
driver.failAllocation = true;
|
||||
const auto saved = driver.state;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
for (const auto& row : measurement.uploads) {
|
||||
EXPECT_EQ(row.unmapped.status, BufferOrderingProbeStatus::Failed);
|
||||
EXPECT_EQ(row.unmapped.error, GLenum(GL_OUT_OF_MEMORY));
|
||||
}
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
EXPECT_EQ(driver.state, saved);
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, ReadbackErrorAfterAMismatchDoesNotProduceAFinding) {
|
||||
driver.corruptSubData = true;
|
||||
driver.failReadbackAt = 82; // Eighty clean control readbacks, then one corrupt subject FBO.
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
EXPECT_GT(measurement.uploads[0].mapped.badFrames, 0u);
|
||||
EXPECT_EQ(measurement.uploads[0].mapped.status, BufferOrderingProbeStatus::Failed);
|
||||
EXPECT_EQ(measurement.uploads[0].mapped.error, GLenum(GL_INVALID_OPERATION));
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
}
|
||||
|
||||
TEST_F(PersistentBufferOrderingProbeTest, IncompleteFramebufferIsInconclusiveAndRestoresBindings) {
|
||||
driver.failFramebuffer = true;
|
||||
const auto saved = driver.state;
|
||||
const auto measurement = ProbePersistentBufferUpdateOrdering(Table());
|
||||
EXPECT_FALSE(DescribePersistentBufferOrderingBug(measurement));
|
||||
EXPECT_EQ(driver.arenaAllocations, 0u);
|
||||
EXPECT_EQ(driver.state, saved);
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
// MobileGL - MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.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
|
||||
//
|
||||
// The primitives-generated-without-transform-feedback probe's VERDICT and ARMING
|
||||
// logic, pinned over synthetic measurements. Recording the probe for real needs a
|
||||
// GPU; the two pure functions are where the cheap mistakes live - a verdict that reads a
|
||||
// half-broken driver as healthy, an override arm swapped so ForceOn disarms, a
|
||||
// substitute ranked below a worse one - and every driver the campaign has
|
||||
// characterised is written down here as a fake measurement so the mapping cannot
|
||||
// drift without a red:
|
||||
// - a conforming driver (stream counts everywhere),
|
||||
// - Mesa lavapipe as measured 2026-08: stream silent everywhere, the dedicated
|
||||
// VK_EXT_primitives_generated_query exact everywhere (discard included), and
|
||||
// the statistics control exact on the plain shape but dead under rasterizer
|
||||
// discard (llvmpipe's discard short-circuit),
|
||||
// - the same driver without the dedicated query - the statistics tiers,
|
||||
// - a device with the defect and no working substitute,
|
||||
// - a substitute that would be WORSE than the stream query on some shape (the
|
||||
// never-worse rule the plain-only arm has to prove before it may arm),
|
||||
// - and the refuse-to-guess shapes (half counts, missing mandatory shapes).
|
||||
//
|
||||
// The last section pins the probe's TEARDOWN CONTRACT instead, driving the real
|
||||
// RunPrimitivesGeneratedNoXfbProbe against a fake Vulkan driver whose fence wait
|
||||
// can be made to expire: no GPU is needed for that, only the entry points the
|
||||
// probe is handed, and what it does on that path is what keeps a hung driver from
|
||||
// hanging the POST.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||
|
||||
using MobileGL::Bool;
|
||||
using MobileGL::Uint32;
|
||||
using MobileGL::Uint64;
|
||||
using MobileGL::MG_Config::QuirkOverride;
|
||||
using MobileGL::MG_Util::SelfTest::EvaluatePrimitivesGeneratedNoXfbVerdict;
|
||||
using MobileGL::MG_Util::SelfTest::ChoosePrimitivesGeneratedReroute;
|
||||
using MobileGL::MG_Util::SelfTest::PrimGenRerouteKind;
|
||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbMeasurement;
|
||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbProbeContext;
|
||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement;
|
||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbVerdict;
|
||||
using MobileGL::MG_Util::SelfTest::RunPrimitivesGeneratedNoXfbProbe;
|
||||
|
||||
namespace {
|
||||
struct ShapeAnswers {
|
||||
Uint64 stream = 0;
|
||||
// Negative-free encoding: measured flags separate from values.
|
||||
Bool pgqMeasured = false;
|
||||
Uint64 pgq = 0;
|
||||
Bool statMeasured = false;
|
||||
Uint64 stat = 0;
|
||||
};
|
||||
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement Shape(const ShapeAnswers& answers) {
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement shape;
|
||||
shape.drawn = true;
|
||||
shape.expectedPrimitives = 1;
|
||||
shape.streamGenerated = answers.stream;
|
||||
shape.primitivesGeneratedExtMeasured = answers.pgqMeasured;
|
||||
shape.primitivesGeneratedExt = answers.pgq;
|
||||
shape.statisticsMeasured = answers.statMeasured;
|
||||
shape.statisticsClippingInput = answers.stat;
|
||||
return shape;
|
||||
}
|
||||
|
||||
PrimitivesGeneratedNoXfbMeasurement Measurement(PrimitivesGeneratedNoXfbShapeMeasurement plain,
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement discard,
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement patches) {
|
||||
PrimitivesGeneratedNoXfbMeasurement measurement;
|
||||
measurement.ran = true;
|
||||
measurement.trianglesPlain = plain;
|
||||
measurement.trianglesDiscard = discard;
|
||||
measurement.patchesDiscard = patches;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement NotDrawn() {
|
||||
return PrimitivesGeneratedNoXfbShapeMeasurement{};
|
||||
}
|
||||
|
||||
constexpr ShapeAnswers kHealthy{1, true, 1, true, 1};
|
||||
// The lavapipe measurement: stream silent, dedicated query exact, statistics
|
||||
// exact only where nothing is discarded.
|
||||
constexpr ShapeAnswers kLavapipePlain{0, true, 1, true, 1};
|
||||
constexpr ShapeAnswers kLavapipeDiscard{0, true, 1, true, 0};
|
||||
} // namespace
|
||||
|
||||
// A conforming driver: the stream query counts every capture-less shape exactly.
|
||||
// Controls agreeing changes nothing - health is decided by the subject.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, AConformingDriverReadsStreamCounts) {
|
||||
const auto measurement = Measurement(Shape(kHealthy), Shape(kHealthy), Shape(kHealthy));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||
}
|
||||
|
||||
// ...and stays healthy with no tessellation stage to draw the patches shape with,
|
||||
// and with no control at all - a control is only required to QUALIFY a
|
||||
// substitute, never to certify health.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, HealthNeedsNeitherTessellationNorAControl) {
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape(kHealthy), Shape(kHealthy), NotDrawn())),
|
||||
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({1}), Shape({1}), Shape({1}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||
}
|
||||
|
||||
// Mesa lavapipe as measured (2026-08): stream silent for every capture-less
|
||||
// draw, the dedicated primitives-generated query exact on every shape (discard
|
||||
// included), the statistics control dead under discard. The dedicated query must
|
||||
// win - it is the only substitute that covers the CTS shape there.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, LavapipeShapedMeasurementTakesTheDedicatedQuery) {
|
||||
const auto measurement =
|
||||
Measurement(Shape(kLavapipePlain), Shape(kLavapipeDiscard), Shape(kLavapipeDiscard));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute);
|
||||
}
|
||||
|
||||
// The affected-device hypothesis with no dedicated query: statistics exact on
|
||||
// every shape, the CTS's discarded shapes included.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsExactEverywhereIsTheFullStatisticsSubstitute) {
|
||||
const auto measurement = Measurement(Shape({0, false, 0, true, 1}), Shape({0, false, 0, true, 1}),
|
||||
Shape({0, false, 0, true, 1}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute);
|
||||
}
|
||||
|
||||
// A dedicated query that is silent in the same way the stream query is must not
|
||||
// be armed - the statistics tier decides instead.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, ASilentDedicatedQueryFallsThroughToStatistics) {
|
||||
const auto measurement = Measurement(Shape({0, true, 0, true, 1}), Shape({0, true, 0, true, 1}),
|
||||
Shape({0, true, 0, true, 1}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute);
|
||||
}
|
||||
|
||||
// The llvmpipe statistics hole without the dedicated query to rescue it: exact on
|
||||
// the plain shape, dead under discard. Repairs undiscarded queries only, and the
|
||||
// verdict must say so.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsDeadUnderDiscardIsThePlainOnlySubstitute) {
|
||||
const auto measurement = Measurement(Shape({0, false, 0, true, 1}), Shape({0, false, 0, true, 0}),
|
||||
Shape({0, false, 0, true, 0}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
|
||||
}
|
||||
|
||||
// THE DOMINATION RULE. The plain-only substitute is armed for EVERY XFB-inactive
|
||||
// draw, so it may only be armed where it is never worse than what it replaces:
|
||||
// each shape it gets wrong must be one the stream query already answered 0 for.
|
||||
// Here the discarded triangle is one the stream query answers EXACTLY (a driver
|
||||
// whose silence is selective) and whose statistics read 0 - rerouting would turn
|
||||
// that correct 1 into a 0, so the honest verdict is that nothing may be armed.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, ASubstituteWorseThanTheStreamOnAnyShapeIsRefused) {
|
||||
const auto measurement = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, true, 0}),
|
||||
Shape({0, false, 0, true, 0}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||
// The same shape with the statistics slot MISSING on the stream-exact shape is
|
||||
// the same trade: an unmeasured control cannot be assumed to answer.
|
||||
const auto unmeasured = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, false, 0}),
|
||||
Shape({0, false, 0, true, 0}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(unmeasured),
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||
// ...while the same selective silence WITH a substitute that covers the shapes
|
||||
// it must still qualifies: every shape the statistics miss read 0 anyway.
|
||||
const auto dominating = Measurement(Shape({1, false, 0, true, 1}), Shape({0, false, 0, true, 1}),
|
||||
Shape({0, false, 0, true, 0}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(dominating),
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
|
||||
}
|
||||
|
||||
// The defect with no substitute: no control, controls silent, or a control that
|
||||
// OVERCOUNTS the plain shape (as disqualifying as one that reads 0 - an exact
|
||||
// match is what qualifies a substitute).
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, StreamSilentWithoutAWorkingPlainControlIsUnfixable) {
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({0}), Shape({0}), Shape({0}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({0, true, 0, true, 0}), Shape({0, true, 0, true, 0}),
|
||||
Shape({0, true, 0, true, 0}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({0, true, 2, true, 2}), Shape({0, true, 1, true, 1}),
|
||||
Shape({0, true, 1, true, 1}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable);
|
||||
}
|
||||
|
||||
// Refuse-to-guess shapes. A nonzero-but-wrong stream answer fits neither the
|
||||
// defect (exact silence) nor health (the exact count), whichever shape carries
|
||||
// it; and a probe that never ran, or lost its mandatory shapes, says nothing.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, AnswersFittingNeitherHealthNorTheDefectAreInconclusive) {
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({2, true, 1, true, 1}), Shape({0, true, 1, true, 1}),
|
||||
Shape({0, true, 1, true, 1}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
Measurement(Shape({0, true, 1, true, 1}), Shape({3, true, 1, true, 1}),
|
||||
Shape({0, true, 1, true, 1}))),
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||
|
||||
PrimitivesGeneratedNoXfbMeasurement neverRan;
|
||||
neverRan.ran = false;
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(neverRan),
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||
|
||||
const auto missingMandatoryShape = Measurement(Shape({0, true, 1, true, 1}), NotDrawn(), NotDrawn());
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(missingMandatoryShape),
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||
}
|
||||
|
||||
// A partial silence is still the defect: the plain shape counts but the discarded
|
||||
// ones read 0 (a driver that gates the stream counter on rasterization rather
|
||||
// than on the capture). With a whole control the substitute is whole.
|
||||
TEST(PrimitivesGeneratedNoXfbVerdictTest, SilenceOnOnlyTheDiscardShapesIsStillTheDefect) {
|
||||
const auto measurement = Measurement(Shape({1, true, 1, true, 1}), Shape({0, true, 1, true, 1}),
|
||||
Shape({0, true, 1, true, 1}));
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute);
|
||||
}
|
||||
|
||||
// ===================== THE OVERRIDE MAPPING =====================
|
||||
//
|
||||
// The one-line swap this exists to catch: ForceOn and ForceOff exchanging arms,
|
||||
// Auto arming on a verdict that never qualified a substitute, or the pool ranking
|
||||
// inverting. Every cell of the (override x verdict) table is written out.
|
||||
|
||||
namespace {
|
||||
constexpr PrimitivesGeneratedNoXfbVerdict kAllVerdicts[] = {
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive,
|
||||
PrimitivesGeneratedNoXfbVerdict::StreamCounts,
|
||||
PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute,
|
||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly,
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable,
|
||||
};
|
||||
}
|
||||
|
||||
TEST(PrimitivesGeneratedNoXfbArmingTest, ForceOffNeverReroutes) {
|
||||
for (const auto verdict : kAllVerdicts) {
|
||||
for (const Bool pgqUsable : {false, true}) {
|
||||
for (const Bool statsUsable : {false, true}) {
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOff, verdict, pgqUsable,
|
||||
statsUsable),
|
||||
PrimGenRerouteKind::None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PrimitivesGeneratedNoXfbArmingTest, ForceOnBypassesTheVerdictButNeverTheStructuralChecks) {
|
||||
for (const auto verdict : kAllVerdicts) {
|
||||
// The dedicated query wins where the device can host it...
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, true, true),
|
||||
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, true, false),
|
||||
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||
// ...statistics stand in where only they exist...
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, false, true),
|
||||
PrimGenRerouteKind::ClippingStatistics);
|
||||
// ...and no pool means no reroute, forced or not.
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::ForceOn, verdict, false, false),
|
||||
PrimGenRerouteKind::None);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PrimitivesGeneratedNoXfbArmingTest, AutoFollowsExactlyTheSubstituteVerdicts) {
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||
true, true),
|
||||
PrimGenRerouteKind::PrimitivesGeneratedExt);
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, true),
|
||||
PrimGenRerouteKind::ClippingStatistics);
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly,
|
||||
false, true),
|
||||
PrimGenRerouteKind::ClippingStatistics);
|
||||
// The statistics verdicts never take the dedicated pool: that verdict only
|
||||
// exists when the dedicated query did NOT qualify.
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, true, true),
|
||||
PrimGenRerouteKind::ClippingStatistics);
|
||||
for (const auto verdict :
|
||||
{PrimitivesGeneratedNoXfbVerdict::Inconclusive, PrimitivesGeneratedNoXfbVerdict::StreamCounts,
|
||||
PrimitivesGeneratedNoXfbVerdict::Unfixable}) {
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(QuirkOverride::Auto, verdict, true, true),
|
||||
PrimGenRerouteKind::None);
|
||||
}
|
||||
// The structural checks bind Auto too.
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute,
|
||||
false, true),
|
||||
PrimGenRerouteKind::None);
|
||||
EXPECT_EQ(ChoosePrimitivesGeneratedReroute(
|
||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, false),
|
||||
PrimGenRerouteKind::None);
|
||||
}
|
||||
|
||||
// ===================== THE FENCE-TIMEOUT CONTRACT =====================
|
||||
//
|
||||
// A driver whose queue never signals the probe's fence inside 5 s is the one case
|
||||
// where the probe must NOT clean up: the submission may still be executing, so
|
||||
// vkDeviceWaitIdle can block forever and destroying in-flight objects is
|
||||
// undefined. It therefore leaks everything it made and says so in the measurement
|
||||
// (`fenceWaitTimedOut`), which is what lets its callers make the same choice for
|
||||
// the object THEY own - the driver POST leaks its throwaway VkDevice instead of
|
||||
// destroying it under live children (vkDestroyDevice would be the very hang the
|
||||
// bound exists to prevent), and the renderer, whose device is the real one, must
|
||||
// not idle-wait it either. Neither guard is reachable from a unit test - the POST
|
||||
// probe lives in an anonymous namespace and the renderer needs a GPU - so this
|
||||
// pins the contract they both key on, at the boundary where it is produced.
|
||||
//
|
||||
// The fake driver below is the whole Vulkan surface the probe touches, with a
|
||||
// dialable fence-wait result and per-entry-point call counters.
|
||||
|
||||
namespace {
|
||||
struct FakeDriverState {
|
||||
VkResult fenceWaitResult = VK_SUCCESS;
|
||||
Uint32 objectsCreated = 0;
|
||||
Uint32 destroyCalls = 0;
|
||||
Uint32 deviceWaitIdleCalls = 0;
|
||||
Uint32 queueSubmitCalls = 0;
|
||||
Uint64 streamGenerated = 1;
|
||||
};
|
||||
FakeDriverState g_fake;
|
||||
|
||||
template <typename Handle>
|
||||
Handle FakeHandle() {
|
||||
++g_fake.objectsCreated;
|
||||
// One cast form for both handle flavours: a pointer on 64-bit builds, a
|
||||
// uint64_t on 32-bit ones. The probe only ever compares against
|
||||
// VK_NULL_HANDLE, so any distinct nonzero value will do.
|
||||
return (Handle)(std::uintptr_t)(0x1000u + g_fake.objectsCreated * 0x10u);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateCommandPool(VkDevice, const VkCommandPoolCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkCommandPool* out) {
|
||||
*out = FakeHandle<VkCommandPool>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyCommandPool(VkDevice, VkCommandPool, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*,
|
||||
VkCommandBuffer* out) {
|
||||
*out = FakeHandle<VkCommandBuffer>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) {
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateQueryPool(VkDevice, const VkQueryPoolCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkQueryPool* out) {
|
||||
*out = FakeHandle<VkQueryPool>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyQueryPool(VkDevice, VkQueryPool, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdResetQueryPool(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQuery(VkCommandBuffer, VkQueryPool, uint32_t, VkQueryControlFlags) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQuery(VkCommandBuffer, VkQueryPool, uint32_t) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t,
|
||||
VkQueryControlFlags, uint32_t) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateRenderPass(VkDevice, const VkRenderPassCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkRenderPass* out) {
|
||||
*out = FakeHandle<VkRenderPass>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyRenderPass(VkDevice, VkRenderPass, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFramebuffer(VkDevice, const VkFramebufferCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkFramebuffer* out) {
|
||||
*out = FakeHandle<VkFramebuffer>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyFramebuffer(VkDevice, VkFramebuffer, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginRenderPass(VkCommandBuffer, const VkRenderPassBeginInfo*,
|
||||
VkSubpassContents) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdEndRenderPass(VkCommandBuffer) {}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkShaderModule* out) {
|
||||
*out = FakeHandle<VkShaderModule>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo*,
|
||||
const VkAllocationCallbacks*,
|
||||
VkPipelineLayout* out) {
|
||||
*out = FakeHandle<VkPipelineLayout>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipelineLayout(VkDevice, VkPipelineLayout,
|
||||
const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateGraphicsPipelines(VkDevice, VkPipelineCache, uint32_t count,
|
||||
const VkGraphicsPipelineCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkPipeline* out) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
out[i] = FakeHandle<VkPipeline>();
|
||||
}
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline) {}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCmdDraw(VkCommandBuffer, uint32_t, uint32_t, uint32_t, uint32_t) {}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFence(VkDevice, const VkFenceCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkFence* out) {
|
||||
*out = FakeHandle<VkFence>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {
|
||||
++g_fake.destroyCalls;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeQueueSubmit(VkQueue, uint32_t, const VkSubmitInfo*, VkFence) {
|
||||
++g_fake.queueSubmitCalls;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeWaitForFences(VkDevice, uint32_t, const VkFence*, VkBool32, uint64_t) {
|
||||
return g_fake.fenceWaitResult;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeGetQueryPoolResults(VkDevice, VkQueryPool, uint32_t, uint32_t,
|
||||
size_t dataSize, void* data, VkDeviceSize,
|
||||
VkQueryResultFlags) {
|
||||
// The stream pool's {primitivesWritten, primitivesNeeded} pair; the probe
|
||||
// reads primitivesNeeded, and this fake device counts capture-less draws.
|
||||
if (data == nullptr || dataSize < 2 * sizeof(Uint64)) {
|
||||
return VK_INCOMPLETE;
|
||||
}
|
||||
auto* pair = static_cast<Uint64*>(data);
|
||||
pair[0] = 0;
|
||||
pair[1] = g_fake.streamGenerated;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeDeviceWaitIdle(VkDevice) {
|
||||
++g_fake.deviceWaitIdleCalls;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
PrimitivesGeneratedNoXfbProbeContext FakeProbeContext() {
|
||||
g_fake = FakeDriverState{};
|
||||
PrimitivesGeneratedNoXfbProbeContext context;
|
||||
context.device = (VkDevice)(std::uintptr_t)0xD0D0;
|
||||
context.queue = (VkQueue)(std::uintptr_t)0xC0C0;
|
||||
context.transformFeedbackQueriesUsable = true;
|
||||
// No controls and no tessellation: this fixture is about the teardown
|
||||
// contract, and the fewer optional slots the fewer moving parts.
|
||||
auto& fns = context.fns;
|
||||
fns.vkCreateCommandPool = FakeCreateCommandPool;
|
||||
fns.vkDestroyCommandPool = FakeDestroyCommandPool;
|
||||
fns.vkAllocateCommandBuffers = FakeAllocateCommandBuffers;
|
||||
fns.vkBeginCommandBuffer = FakeBeginCommandBuffer;
|
||||
fns.vkEndCommandBuffer = FakeEndCommandBuffer;
|
||||
fns.vkCreateQueryPool = FakeCreateQueryPool;
|
||||
fns.vkDestroyQueryPool = FakeDestroyQueryPool;
|
||||
fns.vkCmdResetQueryPool = FakeCmdResetQueryPool;
|
||||
fns.vkCmdBeginQuery = FakeCmdBeginQuery;
|
||||
fns.vkCmdEndQuery = FakeCmdEndQuery;
|
||||
fns.vkCmdBeginQueryIndexedEXT = FakeCmdBeginQueryIndexedEXT;
|
||||
fns.vkCmdEndQueryIndexedEXT = FakeCmdEndQueryIndexedEXT;
|
||||
fns.vkCreateRenderPass = FakeCreateRenderPass;
|
||||
fns.vkDestroyRenderPass = FakeDestroyRenderPass;
|
||||
fns.vkCreateFramebuffer = FakeCreateFramebuffer;
|
||||
fns.vkDestroyFramebuffer = FakeDestroyFramebuffer;
|
||||
fns.vkCmdBeginRenderPass = FakeCmdBeginRenderPass;
|
||||
fns.vkCmdEndRenderPass = FakeCmdEndRenderPass;
|
||||
fns.vkCreateShaderModule = FakeCreateShaderModule;
|
||||
fns.vkDestroyShaderModule = FakeDestroyShaderModule;
|
||||
fns.vkCreatePipelineLayout = FakeCreatePipelineLayout;
|
||||
fns.vkDestroyPipelineLayout = FakeDestroyPipelineLayout;
|
||||
fns.vkCreateGraphicsPipelines = FakeCreateGraphicsPipelines;
|
||||
fns.vkDestroyPipeline = FakeDestroyPipeline;
|
||||
fns.vkCmdBindPipeline = FakeCmdBindPipeline;
|
||||
fns.vkCmdDraw = FakeCmdDraw;
|
||||
fns.vkCreateFence = FakeCreateFence;
|
||||
fns.vkDestroyFence = FakeDestroyFence;
|
||||
fns.vkQueueSubmit = FakeQueueSubmit;
|
||||
fns.vkWaitForFences = FakeWaitForFences;
|
||||
fns.vkGetQueryPoolResults = FakeGetQueryPoolResults;
|
||||
fns.vkDeviceWaitIdle = FakeDeviceWaitIdle;
|
||||
return context;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The hung driver. Nothing the probe created may be destroyed, the device may not
|
||||
// be idle-waited, and the measurement must SAY the wait timed out - a caller that
|
||||
// owns the device reads that flag to leak it too, and `ran == false` alone cannot
|
||||
// tell this apart from an ordinary setup failure (where teardown already ran and
|
||||
// destroying the device is correct).
|
||||
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, AFenceTimeoutLeaksEverythingAndReportsItself) {
|
||||
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
|
||||
g_fake.fenceWaitResult = VK_TIMEOUT;
|
||||
|
||||
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
|
||||
|
||||
EXPECT_FALSE(measurement.ran);
|
||||
EXPECT_TRUE(measurement.fenceWaitTimedOut)
|
||||
<< "without this flag the POST destroys its throwaway VkDevice while the probe's children "
|
||||
"are alive and its submission may still be executing";
|
||||
EXPECT_GT(g_fake.queueSubmitCalls, 0u) << "the timeout must be the SUBMITTED probe's, not a setup failure";
|
||||
EXPECT_EQ(g_fake.destroyCalls, 0u)
|
||||
<< "a probe that timed out must destroy nothing: the submission may still be executing";
|
||||
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 0u)
|
||||
<< "vkDeviceWaitIdle on a queue that missed a 5 s deadline is the hang the bound exists to "
|
||||
"prevent";
|
||||
// The verdict must not read a timed-out probe as anything but "no verdict".
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
|
||||
}
|
||||
|
||||
// The control: a driver that signals normally gets the ordinary teardown - idle
|
||||
// wait, every object destroyed, no timeout flag - so the case above is testing the
|
||||
// timeout branch and not a probe that never cleans up at all.
|
||||
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, ASignalledFenceTearsDownNormally) {
|
||||
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
|
||||
g_fake.fenceWaitResult = VK_SUCCESS;
|
||||
g_fake.streamGenerated = 1; // healthy: the capture-less draws are counted
|
||||
|
||||
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
|
||||
|
||||
EXPECT_TRUE(measurement.ran) << measurement.failureReason;
|
||||
EXPECT_FALSE(measurement.fenceWaitTimedOut);
|
||||
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 1u);
|
||||
EXPECT_GT(g_fake.destroyCalls, 0u);
|
||||
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
|
||||
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
|
||||
}
|
||||
@@ -11,12 +11,14 @@ add_executable(
|
||||
FlattenFloat64StorageBlockTest.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
StripIoBlockLocationsTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
WidenImageFormatsTest.cpp
|
||||
GlslangCaptureTest.cpp
|
||||
DemotePointSizeTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.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 <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include "spirv-tools/libspirv.hpp"
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
// Compiles and LINKS a whole program, then returns one sanitized module per stage - the
|
||||
// exact bytes ProgramSpirvTask hands the demotion in production, so every shape assertion
|
||||
// below is made against what the backends would really receive.
|
||||
Vector<Vector<Uint32>> CompileProgramToSpirv(const Vector<Pair<GLenum, const char*>>& stages) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
Vector<GLenum> types;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
// sourceStr is a StringView; the literals handed in are static, so the view
|
||||
// stays valid for the whole compile.
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
shaders.push_back(shaderResult.value());
|
||||
types.push_back(stage);
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult) return {};
|
||||
Vector<Vector<Uint32>> modules = Move(binaryResult.value());
|
||||
for (auto& module : modules) {
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
EXPECT_TRUE(tools.Disassemble(spirv, &text));
|
||||
return text;
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
Bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
// The five-stage shape of the KHR-GL4x transform-feedback / tessellation capture bodies:
|
||||
// the value is WRITTEN in the vertex stage, READ from gl_in and re-written in every stage
|
||||
// after it, and the rasterized size never matters (the captures run under rasterizer
|
||||
// discard). This is exactly the class the demotion exists to rescue.
|
||||
const char* kVertexSource = R"(#version 460 core
|
||||
void main() {
|
||||
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 2.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kTessControlSource = 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* kTessEvalSource = R"(#version 460 core
|
||||
layout(triangles, point_mode) in;
|
||||
void main() {
|
||||
gl_Position = gl_TessCoord.x * gl_in[0].gl_Position + gl_TessCoord.y * gl_in[1].gl_Position +
|
||||
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kGeometrySource = 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();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kFragmentSource = R"(#version 460 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
// A control chain that never touches point size: the demotion must prove it changed
|
||||
// NOTHING here, byte for byte, because this is the overwhelming majority of programs on
|
||||
// an affected device.
|
||||
const char* kPlainTessControlSource = R"(#version 460 core
|
||||
layout(vertices = 3) out;
|
||||
void main() {
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kPlainTessEvalSource = R"(#version 460 core
|
||||
layout(triangles, point_mode) in;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kPlainVertexSource = R"(#version 460 core
|
||||
void main() {
|
||||
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A control stage that also carries CLIP DISTANCE. SPIRV-Cross force-redeclares the whole
|
||||
// gl_PerVertex output block for exactly this stage/builtin combination, and prints its
|
||||
// members from the struct's DECORATIONS rather than from what the module accesses - so a
|
||||
// demoted module's untouched PointSize member would still reach the driver's ESSL.
|
||||
const char* kClipDistanceTessControlSource = 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_out[gl_InvocationID].gl_ClipDistance[0] = 0.5;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The same clip-distance write and NO point-size access anywhere: the shape a
|
||||
// successfully demoted module would have been left in. glslang emits the whole
|
||||
// four-member gl_PerVertex block regardless, which is what makes it the exact
|
||||
// "declared but unaccessed" state the pass header's premise is about.
|
||||
const char* kClipDistanceUnusedPointSizeTessControlSource = 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_ClipDistance[0] = 0.5;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// A tessellation evaluation module reaching PointSize through a WHOLE-STRUCT load - the
|
||||
// one shape the pass must refuse rather than half-rewrite. glslang never emits it, so it
|
||||
// is assembled by hand.
|
||||
const char* kWholeStructCopyTessEvalAsm = R"(
|
||||
OpCapability Tessellation
|
||||
OpCapability TessellationPointSize
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint TessellationEvaluation %main "main" %gl_in %out_block
|
||||
OpExecutionMode %main Triangles
|
||||
OpExecutionMode %main SpacingEqual
|
||||
OpExecutionMode %main VertexOrderCcw
|
||||
OpMemberDecorate %gl_PerVertex 0 BuiltIn Position
|
||||
OpMemberDecorate %gl_PerVertex 1 BuiltIn PointSize
|
||||
OpDecorate %gl_PerVertex Block
|
||||
%void = OpTypeVoid
|
||||
%fn_ty = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%gl_PerVertex = OpTypeStruct %v4float %float
|
||||
%uint = OpTypeInt 32 0
|
||||
%uint_32 = OpConstant %uint 32
|
||||
%arr = OpTypeArray %gl_PerVertex %uint_32
|
||||
%ptr_in_arr = OpTypePointer Input %arr
|
||||
%gl_in = OpVariable %ptr_in_arr Input
|
||||
%ptr_out_s = OpTypePointer Output %gl_PerVertex
|
||||
%out_block = OpVariable %ptr_out_s Output
|
||||
%ptr_in_s = OpTypePointer Input %gl_PerVertex
|
||||
%int = OpTypeInt 32 1
|
||||
%int_0 = OpConstant %int 0
|
||||
%main = OpFunction %void None %fn_ty
|
||||
%entry = OpLabel
|
||||
%p = OpAccessChain %ptr_in_s %gl_in %int_0
|
||||
%v = OpLoad %gl_PerVertex %p
|
||||
OpStore %out_block %v
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class DemotePointSizeTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
m_validationFailuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresBefore)
|
||||
<< "a demoted module did not survive spirv-val";
|
||||
}
|
||||
|
||||
private:
|
||||
Uint64 m_validationFailuresBefore = 0;
|
||||
};
|
||||
|
||||
TEST_F(DemotePointSizeTest, DemotesAFiveStageProgramWholesale) {
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||
{GL_GEOMETRY_SHADER, kGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 5u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||
|
||||
// The defect, pinned first: every tessellation/geometry stage really does declare the
|
||||
// capability the device lacks - the same probe production's declines use.
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2]));
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3]));
|
||||
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
// THE PRODUCTION GATE, as the arming guard: after demotion neither decline can arm.
|
||||
// Magma's refusal and Espryt's missing-extension failure both key off exactly these.
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2]));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3]));
|
||||
for (const auto& module : modules) {
|
||||
EXPECT_TRUE(Validates(module));
|
||||
}
|
||||
|
||||
// The carrier chain, boundary by boundary. No user varyings, so the shared location is 0.
|
||||
const String vs = Disassemble(modules[0]);
|
||||
EXPECT_NE(vs.find("OpName %mg_PointSizeIo0"), String::npos) << vs;
|
||||
EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos)
|
||||
<< "the vertex stage must mirror its built-in into the carrier:\n"
|
||||
<< vs;
|
||||
EXPECT_NE(vs.find("BuiltIn PointSize"), String::npos)
|
||||
<< "the vertex stage KEEPS its core built-in - only tess/geometry stages demote:\n"
|
||||
<< vs;
|
||||
|
||||
const String tcs = Disassemble(modules[1]);
|
||||
EXPECT_EQ(tcs.find("OpCapability TessellationPointSize"), String::npos) << tcs;
|
||||
EXPECT_NE(tcs.find("OpName %mg_PointSizeIo0"), String::npos) << tcs;
|
||||
EXPECT_NE(tcs.find("OpName %mg_PointSizeIo1"), String::npos) << tcs;
|
||||
|
||||
const String tes = Disassemble(modules[2]);
|
||||
EXPECT_EQ(tes.find("OpCapability TessellationPointSize"), String::npos) << tes;
|
||||
EXPECT_NE(tes.find("OpName %mg_PointSizeIo1"), String::npos) << tes;
|
||||
EXPECT_NE(tes.find("OpName %mg_PointSizeIo2"), String::npos)
|
||||
<< "with a geometry stage present the evaluation stage feeds the Io2 boundary, not the "
|
||||
"capture carrier:\n"
|
||||
<< tes;
|
||||
|
||||
const String gs = Disassemble(modules[3]);
|
||||
EXPECT_EQ(gs.find("OpCapability GeometryPointSize"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpName %mg_PointSizeIo2"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpName %mg_PointSizeCapture"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 0"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpStore %mg_PointSizeCapture"), String::npos) << gs;
|
||||
|
||||
// The struct keeps its member - declared, decorated, unaccessed - which is the shape a
|
||||
// point-size-free glslang module already has on every extension-less driver.
|
||||
EXPECT_NE(tes.find("BuiltIn PointSize"), String::npos) << tes;
|
||||
|
||||
// What SPIRV-Cross then prints: no gl_PointSize anywhere in a demoted stage's ESSL (the
|
||||
// token DirectGLES's extension gate greps for), the carriers in its place. The CONTROL
|
||||
// stage is transpiled too, and deliberately: it is the one stage SPIRV-Cross can be made
|
||||
// to redeclare the whole output block for, which is why the clip-distance combination
|
||||
// declines instead of demoting.
|
||||
const String tcsEssl = Transpile(modules[1]);
|
||||
EXPECT_EQ(tcsEssl.find("gl_PointSize"), String::npos) << tcsEssl;
|
||||
EXPECT_NE(tcsEssl.find("mg_PointSizeIo1"), String::npos) << tcsEssl;
|
||||
const String tesEssl = Transpile(modules[2]);
|
||||
EXPECT_EQ(tesEssl.find("gl_PointSize"), String::npos) << tesEssl;
|
||||
EXPECT_NE(tesEssl.find("mg_PointSizeIo1"), String::npos) << tesEssl;
|
||||
const String gsEssl = Transpile(modules[3]);
|
||||
EXPECT_EQ(gsEssl.find("gl_PointSize"), String::npos) << gsEssl;
|
||||
EXPECT_NE(gsEssl.find("mg_PointSizeCapture"), String::npos) << gsEssl;
|
||||
|
||||
// Demotion is idempotent by construction: with the capability gone, a second pass over
|
||||
// the same modules finds nothing to arm on and must not touch a byte.
|
||||
Vector<Vector<Uint32>> again = modules;
|
||||
ShaderCompiler::PointSizeDemotionOutcome secondOutcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
again, types, true, true, true, secondOutcome, true, true));
|
||||
EXPECT_FALSE(secondOutcome.demoted);
|
||||
EXPECT_TRUE(secondOutcome.declineDetail.empty()) << secondOutcome.declineDetail;
|
||||
EXPECT_EQ(again, modules);
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, WithoutAGeometryStageTheEvaluationStageOwnsTheCaptureCarrier) {
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
const String tes = Disassemble(modules[2]);
|
||||
EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes;
|
||||
EXPECT_NE(tes.find("OpStore %mg_PointSizeCapture"), String::npos) << tes;
|
||||
EXPECT_EQ(tes.find("OpName %mg_PointSizeIo2"), String::npos)
|
||||
<< "no geometry stage, no Io2 boundary:\n"
|
||||
<< tes;
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, AGeometryOnlyProgramReadsTheVertexBoundary) {
|
||||
const char* geometryReadingVs = 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();
|
||||
}
|
||||
)";
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometryReadingVs},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, /*demoteTessellation=*/false, /*demoteGeometry=*/true, false, outcome, true,
|
||||
true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
const String gs = Disassemble(modules[1]);
|
||||
EXPECT_NE(gs.find("OpName %mg_PointSizeIo0"), String::npos)
|
||||
<< "the geometry stage's input boundary is fed by the vertex stage:\n"
|
||||
<< gs;
|
||||
const String vs = Disassemble(modules[0]);
|
||||
EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos) << vs;
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, TheCarrierLandsPastTheProgramsOwnVaryings) {
|
||||
const char* vsWithVarying = R"(#version 460 core
|
||||
out vec4 v_color;
|
||||
void main() {
|
||||
gl_Position = vec4(1.0);
|
||||
gl_PointSize = 3.0;
|
||||
v_color = vec4(0.5);
|
||||
}
|
||||
)";
|
||||
const char* gsWithVarying = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
in vec4 v_color[];
|
||||
out vec4 g_color;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize;
|
||||
g_color = v_color[0];
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
const char* fsWithVarying = R"(#version 460 core
|
||||
in vec4 g_color;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = g_color; }
|
||||
)";
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vsWithVarying},
|
||||
{GL_GEOMETRY_SHADER, gsWithVarying},
|
||||
{GL_FRAGMENT_SHADER, fsWithVarying}});
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, false, true, true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
// v_color / g_color occupy location 0, so every carrier must sit at 1 - in every stage,
|
||||
// because producer and consumer match by location.
|
||||
const String vs = Disassemble(modules[0]);
|
||||
EXPECT_NE(vs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << vs;
|
||||
const String gs = Disassemble(modules[1]);
|
||||
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 1"), String::npos) << gs;
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, APointSizeFreeProgramStaysByteIdentical) {
|
||||
Vector<Vector<Uint32>> modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, kPlainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kPlainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
const Vector<Vector<Uint32>> before = modules;
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, false, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.demoted);
|
||||
EXPECT_TRUE(outcome.declineDetail.empty()) << outcome.declineDetail;
|
||||
EXPECT_EQ(modules, before);
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, AHostingDeviceStaysByteIdentical) {
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
const Vector<Vector<Uint32>> before = modules;
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
// Both verdicts say the device hosts the built-in: the un-forced lane's contract.
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, false, false, true, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.demoted);
|
||||
EXPECT_EQ(modules, before);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||
<< "the un-demoted module must still arm the existing declines";
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, ACaptureRequestForcesTheCarrierOnANonWritingCaptureStage) {
|
||||
// The control stage writes point size (arming the demotion); the evaluation stage never
|
||||
// does - but a by-name capture must still find the carrier declared there, holding
|
||||
// whatever an unwritten varying holds, exactly as the unwritten built-in would have.
|
||||
Vector<Vector<Uint32>> modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
const String tes = Disassemble(modules[2]);
|
||||
EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes;
|
||||
EXPECT_TRUE(Validates(modules[2]));
|
||||
|
||||
// And the driver-side half of the same contract: the ESSL DirectGLES hands its driver
|
||||
// has to DECLARE the carrier, because DirectGLES respells the glTransformFeedbackVaryings
|
||||
// request to that name. A carrier the transpile dropped would take the whole capture set
|
||||
// down with an ES link error naming a variable the application never wrote.
|
||||
const String tesEssl = Transpile(modules[2]);
|
||||
EXPECT_NE(tesEssl.find("mg_PointSizeCapture"), String::npos) << tesEssl;
|
||||
}
|
||||
|
||||
// THE PRODUCTION SHAPE THE FORCED CARRIER EXISTS FOR, and the one the flag's own unit test
|
||||
// could not reach: the capture stage never WRITES gl_PointSize, it only reads the incoming
|
||||
// one. The demotion still arms - glslang declares GeometryPointSize on a READ - so the
|
||||
// built-in leaves the module, and only the capture request can put a carrier back. In
|
||||
// production that request arrives as ProgramLinkTask::SpirvHandoff::captureRequestsPointSize;
|
||||
// this is the same value one layer down.
|
||||
TEST_F(DemotePointSizeTest, AReadOnlyCaptureStageStillDeclaresTheCaptureCarrier) {
|
||||
const char* readOnlyGeometry = 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* echoFragment = R"(#version 460 core
|
||||
in float g_echo;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(g_echo); }
|
||||
)";
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_GEOMETRY_SHADER, readOnlyGeometry},
|
||||
{GL_FRAGMENT_SHADER, echoFragment}});
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||
|
||||
// The premise: a stage that only READS the built-in still declares the capability, so the
|
||||
// device still refuses it and the demotion still arms.
|
||||
ASSERT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||
<< "a geometry stage that only reads gl_in[].gl_PointSize must still declare "
|
||||
"GeometryPointSize, or this whole class of program was never affected";
|
||||
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, false, true, /*captureRequestsPointSize=*/true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
const String gs = Disassemble(modules[1]);
|
||||
EXPECT_NE(gs.find("OpName %mg_PointSizeIo0"), String::npos)
|
||||
<< "the read still has to reach the vertex stage's mirrored value:\n"
|
||||
<< gs;
|
||||
EXPECT_NE(gs.find("OpName %mg_PointSizeCapture"), String::npos)
|
||||
<< "the capture request must force the carrier even though this stage never writes "
|
||||
"the built-in; without it DirectGLES respells the capture to a name no stage "
|
||||
"declares and the whole capture set fails to link:\n"
|
||||
<< gs;
|
||||
const String gsEssl = Transpile(modules[1]);
|
||||
EXPECT_NE(gsEssl.find("mg_PointSizeCapture"), String::npos) << gsEssl;
|
||||
EXPECT_EQ(gsEssl.find("gl_PointSize"), String::npos) << gsEssl;
|
||||
|
||||
// Without the request there is nothing to bind a by-name capture to - which is exactly
|
||||
// what production did on every link while the request never reached this call.
|
||||
Vector<Vector<Uint32>> unrequested = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_GEOMETRY_SHADER, readOnlyGeometry},
|
||||
{GL_FRAGMENT_SHADER, echoFragment}});
|
||||
ASSERT_EQ(unrequested.size(), 3u);
|
||||
ShaderCompiler::PointSizeDemotionOutcome unrequestedOutcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
unrequested, types, false, true, /*captureRequestsPointSize=*/false, unrequestedOutcome, true,
|
||||
true));
|
||||
EXPECT_TRUE(unrequestedOutcome.demoted) << unrequestedOutcome.declineDetail;
|
||||
EXPECT_EQ(Disassemble(unrequested[1]).find("OpName %mg_PointSizeCapture"), String::npos)
|
||||
<< "with no capture asking for it, the carrier must not be declared";
|
||||
}
|
||||
|
||||
// THE PREMISE THE PASS HEADER USED TO STATE UNIVERSALLY: "declared but no longer accessed"
|
||||
// is invisible to the ES hop. It is not, for one stage/builtin combination - and this case
|
||||
// pins the mechanism with no demotion involved at all, so a future SPIRV-Cross that emitted
|
||||
// by ACCESS would fail here first and the decline below could be relaxed.
|
||||
TEST_F(DemotePointSizeTest, ARedeclaredControlBlockPrintsAnUnaccessedPointSizeMember) {
|
||||
Vector<Vector<Uint32>> modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, kPlainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kClipDistanceUnusedPointSizeTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
|
||||
// Nothing in this control stage touches point size, so nothing declares the capability -
|
||||
// it is byte-for-byte the state a demoted module would be left in.
|
||||
ASSERT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]));
|
||||
const String tcs = Disassemble(modules[1]);
|
||||
EXPECT_NE(tcs.find("BuiltIn PointSize"), String::npos)
|
||||
<< "the member has to still be declared for this case to say anything:\n"
|
||||
<< tcs;
|
||||
|
||||
const String tcsEssl = Transpile(modules[1]);
|
||||
EXPECT_NE(tcsEssl.find("gl_PointSize"), String::npos)
|
||||
<< "SPIRV-Cross force-redeclares a control stage's gl_PerVertex output block when its "
|
||||
"clip/cull distances are live, and prints the block's members from their "
|
||||
"decorations rather than from what is accessed. DirectGLES's extension gate is a "
|
||||
"text search for this token over exactly this string:\n"
|
||||
<< tcsEssl;
|
||||
}
|
||||
|
||||
// ... and therefore this program declines rather than demoting: a mutated module that the
|
||||
// driver still rejects is strictly worse than the honest refusal, because it also flips the
|
||||
// program-wide verdict and the L1 key.
|
||||
TEST_F(DemotePointSizeTest, AControlStageCarryingClipDistanceDeclinesTheProgram) {
|
||||
Vector<Vector<Uint32>> modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kClipDistanceTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 4u);
|
||||
const Vector<Vector<Uint32>> before = modules;
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, true, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.demoted);
|
||||
EXPECT_NE(outcome.declineDetail.find("clip/cull"), String::npos) << outcome.declineDetail;
|
||||
EXPECT_EQ(modules, before) << "a decline must leave every module byte-identical";
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1]))
|
||||
<< "the declined program must still arm the existing honest refusals";
|
||||
}
|
||||
|
||||
// A legal desktop-GL shape the passthrough machinery explicitly serves: an evaluation stage
|
||||
// sitting straight on the vertex stage. Both backends synthesize the missing control stage,
|
||||
// and that synthesized stage forwards gl_Position and nothing else - so the input carrier the
|
||||
// demotion would create has no producer, and each backend's "reads a located input" guard
|
||||
// would decline the program against a varying name the application never wrote. Declining the
|
||||
// demotion instead keeps the modules, and the diagnostics, honest.
|
||||
TEST_F(DemotePointSizeTest, AnEvaluationStageWithNoControlStageDeclines) {
|
||||
const char* readingTessEval = R"(#version 460 core
|
||||
layout(triangles, point_mode) in;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||
}
|
||||
)";
|
||||
Vector<Vector<Uint32>> modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_TESS_EVALUATION_SHADER, readingTessEval},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
const Vector<Vector<Uint32>> before = modules;
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, true, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.demoted);
|
||||
EXPECT_NE(outcome.declineDetail.find("control stage"), String::npos) << outcome.declineDetail;
|
||||
EXPECT_EQ(modules, before) << "a decline must leave every module byte-identical";
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleReadsLocatedInput(modules[1]))
|
||||
<< "the declined evaluation stage must not have acquired the located input carrier "
|
||||
"that both backends' pass-through guard refuses";
|
||||
}
|
||||
|
||||
// The carrier is placed one past the highest location any stage CONSUMES, and a 64-bit
|
||||
// vector consumes two of them. GL 4.6 core 11.1.2.1 says so for doubles, and
|
||||
// ARB_gpu_shader_int64 - which DirectVulkan advertises unconditionally - extends the rule
|
||||
// verbatim to i64/u64. An i64vec4 counted as one location would put the carrier on the
|
||||
// SECOND location that varying already owns: two Output variables at one location, an
|
||||
// invalid Vulkan interface and an ES link error naming a variable the application never
|
||||
// wrote. This is the one direction the placement is not allowed to be wrong in.
|
||||
TEST_F(DemotePointSizeTest, TheCarrierClearsA64BitIntegerVectorVarying) {
|
||||
const char* wideVertex = R"(#version 460 core
|
||||
#extension GL_ARB_gpu_shader_int64 : require
|
||||
layout(location = 0) flat out i64vec4 v_wide;
|
||||
void main() {
|
||||
gl_Position = vec4(1.0);
|
||||
gl_PointSize = 3.0;
|
||||
v_wide = i64vec4(1, 2, 3, 4);
|
||||
}
|
||||
)";
|
||||
const char* wideGeometry = R"(#version 460 core
|
||||
#extension GL_ARB_gpu_shader_int64 : require
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
layout(location = 0) flat in i64vec4 v_wide[];
|
||||
layout(location = 0) flat out i64vec4 g_wide;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize;
|
||||
g_wide = v_wide[0];
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
const char* wideFragment = R"(#version 460 core
|
||||
#extension GL_ARB_gpu_shader_int64 : require
|
||||
layout(location = 0) flat in i64vec4 g_wide;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(g_wide.x)); }
|
||||
)";
|
||||
Vector<Vector<Uint32>> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, wideVertex},
|
||||
{GL_GEOMETRY_SHADER, wideGeometry},
|
||||
{GL_FRAGMENT_SHADER, wideFragment}});
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, false, true, true, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.demoted) << outcome.declineDetail;
|
||||
|
||||
// v_wide / g_wide sit at location 0 and occupy 0 AND 1, so every carrier must clear 2.
|
||||
const String vs = Disassemble(modules[0]);
|
||||
EXPECT_NE(vs.find("OpDecorate %mg_PointSizeIo0 Location 2"), String::npos)
|
||||
<< "the carrier landed on a location the i64vec4 varying already owns:\n"
|
||||
<< vs;
|
||||
const String gs = Disassemble(modules[1]);
|
||||
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeIo0 Location 2"), String::npos) << gs;
|
||||
EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 2"), String::npos) << gs;
|
||||
}
|
||||
|
||||
TEST_F(DemotePointSizeTest, AWholeStructCopyDeclinesTheProgramByteIdentically) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<Uint32> module;
|
||||
ASSERT_TRUE(tools.Assemble(kWholeStructCopyTessEvalAsm, &module));
|
||||
ASSERT_TRUE(tools.Validate(module));
|
||||
|
||||
Vector<Vector<Uint32>> modules{module};
|
||||
const Vector<GLenum> types{GL_TESS_EVALUATION_SHADER};
|
||||
ShaderCompiler::PointSizeDemotionOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
modules, types, true, true, false, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.demoted);
|
||||
EXPECT_FALSE(outcome.declineDetail.empty())
|
||||
<< "a shape the pass cannot express must say so, not silently no-op";
|
||||
EXPECT_EQ(modules[0], module) << "a decline must not leave a half-demoted module behind";
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[0]))
|
||||
<< "the declined module must still arm the existing honest refusals";
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
@@ -38,6 +40,7 @@ namespace {
|
||||
constexpr Uint32 kOpTypeInt = 21;
|
||||
constexpr Uint32 kOpTypeFloat = 22;
|
||||
constexpr Uint32 kOpTypeArray = 28;
|
||||
constexpr Uint32 kOpTypeRuntimeArray = 29;
|
||||
constexpr Uint32 kOpTypeStruct = 30;
|
||||
constexpr Uint32 kOpConstant = 43;
|
||||
constexpr Uint32 kDecorationArrayStride = 6;
|
||||
@@ -116,6 +119,18 @@ namespace {
|
||||
return {elementTypeId, length};
|
||||
}
|
||||
|
||||
// The element type id of OpTypeRuntimeArray <arrayId>, or 0 when it is not one - which is
|
||||
// what a BOUNDED flattened member (an OpTypeArray) answers too, so the two shapes can be told
|
||||
// apart by the pair of helpers.
|
||||
Uint32 RuntimeArrayElementOf(const Vector<Uint32>& spirv, Uint32 arrayId) {
|
||||
Uint32 elementTypeId = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != kOpTypeRuntimeArray || wordCount < 3 || words[1] != arrayId) return;
|
||||
elementTypeId = words[2];
|
||||
});
|
||||
return elementTypeId;
|
||||
}
|
||||
|
||||
Bool IsUint32Type(const Vector<Uint32>& spirv, Uint32 typeId) {
|
||||
Bool isUint = false;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
@@ -140,6 +155,61 @@ namespace {
|
||||
return text;
|
||||
}
|
||||
|
||||
// How many lines of a disassembly hold BOTH fragments - "OpIMul %uint" and "%uint_8", say -
|
||||
// which is how the index arithmetic the pass emits is pinned without a host that could run it.
|
||||
Uint32 CountLinesWith(const String& text, const String& first, const String& second) {
|
||||
Uint32 count = 0;
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart < text.size()) {
|
||||
SizeT lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == String::npos) lineEnd = text.size();
|
||||
const String line = text.substr(lineStart, lineEnd - lineStart);
|
||||
if (line.find(first) != String::npos && line.find(second) != String::npos) ++count;
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// What every test of the open-ended shape asserts: the block collapsed to ONE member, which
|
||||
// is a `uint[]` RUNTIME array of stride 4 rather than a bounded one, and nothing 64-bit is
|
||||
// left for the demotion to find. Returns the disassembly for the arithmetic checks.
|
||||
String ExpectOpenEndedWordArray(const Vector<Uint32>& output, const String& blockName) {
|
||||
const String text = Disassemble(output);
|
||||
const Uint32 structId = StructIdNamed(output, blockName);
|
||||
EXPECT_NE(structId, 0u) << text;
|
||||
if (structId == 0) return text;
|
||||
const Vector<Uint32> members = MemberTypesOf(output, structId);
|
||||
EXPECT_EQ(members.size(), 1u) << "the block should have collapsed to one member\n" << text;
|
||||
if (members.size() != 1) return text;
|
||||
EXPECT_EQ(MemberOffsetsOf(output, structId), (Vector<Uint32>{0}));
|
||||
const Uint32 elementTypeId = RuntimeArrayElementOf(output, members[0]);
|
||||
EXPECT_NE(elementTypeId, 0u) << "member 0 is not a runtime array\n" << text;
|
||||
EXPECT_EQ(ArrayShapeOf(output, members[0]).first, 0u)
|
||||
<< "an open-ended block must not be given a bounded length\n"
|
||||
<< text;
|
||||
EXPECT_TRUE(IsUint32Type(output, elementTypeId)) << text;
|
||||
EXPECT_EQ(DecorationValueOf(output, members[0], kDecorationArrayStride), 4u) << text;
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << text;
|
||||
return text;
|
||||
}
|
||||
|
||||
// The compute shape every failing KHR-Single-GL45.subgroups fp64 case binds: one runtime
|
||||
// array of doubles, indexed by an invocation id, read whole-element.
|
||||
String OpenEndedComputeSource(const String& elementType) {
|
||||
return String(R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data { )") +
|
||||
elementType + R"( data[]; };
|
||||
void main() {
|
||||
)" + elementType +
|
||||
R"( value = data[gl_LocalInvocationID.x] * data[0];
|
||||
result[gl_GlobalInvocationID.x] = uint(value)" +
|
||||
(elementType == "double" ? String{} : String(".x")) + R"();
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
@@ -345,3 +415,679 @@ TEST_F(FlattenFloat64StorageBlockTest, TheDemotedPathIsUnchangedByTheCapabilityA
|
||||
EXPECT_EQ(explicitlyDemoted, defaulted);
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(defaulted, 64), 0u) << Disassemble(defaulted);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The open-ended shape: a block whose last member is a runtime array. Before this was accepted
|
||||
// the pass declined it and the demotion re-derived ArrayStride 4 for the now-float element, so
|
||||
// `double data[]` read the application's 8-byte-stride buffer as 32-bit words - every fp64
|
||||
// KHR-Single-GL45.subgroups case failed on exactly that.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AnOpenEndedBlockOfDoublesBecomesAWordRuntimeArray) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, OpenEndedComputeSource("double"));
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// Element i of the original array starts at word 2i, so the dynamic index is scaled by 2 ...
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_2"), 1u) << text;
|
||||
// ... and the constant `data[0]` is the pair of words at 0 and 1, reached through the one
|
||||
// member the block has left.
|
||||
EXPECT_GE(CountLinesWith(text, "OpAccessChain %_ptr_StorageBuffer_uint", "%uint_0 %uint_0"), 1u) << text;
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, EachDoubleVectorWidthStepsByItsOwnStride) {
|
||||
struct Shape {
|
||||
const char* element;
|
||||
// std430 strides: dvec2 16 bytes, dvec3 and dvec4 32 bytes - i.e. 4, 8 and 8 words.
|
||||
const char* strideWords;
|
||||
// The last component's word offset inside one element, and the first one past it.
|
||||
const char* lastComponentWords;
|
||||
const char* firstWordPastIt;
|
||||
};
|
||||
const Shape shapes[] = {{"dvec2", "%uint_4", "%uint_2", "%uint_4"},
|
||||
{"dvec3", "%uint_8", "%uint_4", "%uint_6"},
|
||||
{"dvec4", "%uint_8", "%uint_6", "%uint_8"}};
|
||||
for (const Shape& shape : shapes) {
|
||||
SCOPED_TRACE(shape.element);
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, OpenEndedComputeSource(shape.element));
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", shape.strideWords), 1u) << text;
|
||||
EXPECT_GE(CountLinesWith(text, "OpIAdd %uint", shape.lastComponentWords), 1u) << text;
|
||||
// A dvec3 is six words in a stride of eight: nothing may be read from the padding.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", shape.firstWordPastIt), 0u) << text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AFixedPrefixBeforeTheRuntimeArrayIsAddedToEveryIndex) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data {
|
||||
uvec4 head;
|
||||
double data[];
|
||||
};
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = head.x + uint(data[gl_LocalInvocationID.x]);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputStructId = StructIdNamed(input, "Data");
|
||||
ASSERT_NE(inputStructId, 0u);
|
||||
EXPECT_EQ(MemberOffsetsOf(input, inputStructId), (Vector<Uint32>{0, 16})) << Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// The 16-byte prefix is 4 words: element i is at word 4 + 2i.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_2"), 1u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", "%uint_4"), 1u) << text;
|
||||
// And the prefix member itself is still word 0.
|
||||
EXPECT_GE(CountLinesWith(text, "OpAccessChain %_ptr_StorageBuffer_uint", "%uint_0 %uint_0"), 1u) << text;
|
||||
}
|
||||
|
||||
// OpArrayLength on the flattened member counts WORDS. GL's `.length()` is the number of whole
|
||||
// elements the bound range holds past the array's offset, so the count has to be rebased and
|
||||
// divided - in unsigned arithmetic, and clamped rather than wrapped when the range is shorter
|
||||
// than the prefix.
|
||||
namespace {
|
||||
// A prefix, an open-ended array of doubles, and a `.length()` of it - the one shape whose
|
||||
// rewrite is an instruction SPIRV-Cross has to spell rather than plain arithmetic.
|
||||
constexpr const char* kOpenEndedLengthSource = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data {
|
||||
uvec4 head;
|
||||
double data[];
|
||||
};
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = uint(data.length()) + head.y;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheLengthOfAnOpenEndedBlockIsRewrittenToAnElementCount) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kOpenEndedLengthSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// glslang asks for member 1's length and signs the answer.
|
||||
EXPECT_EQ(CountLinesWith(Disassemble(input), "OpArrayLength %uint", " 1"), 1u) << Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// Re-aimed at the one member left, ...
|
||||
EXPECT_EQ(CountLinesWith(text, "OpArrayLength %uint", " 0"), 1u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpArrayLength %uint", " 1"), 0u) << text;
|
||||
// ... rebased past the 4-word prefix, clamped at zero when the range does not reach it, ...
|
||||
EXPECT_EQ(CountLinesWith(text, "OpISub %uint", "%uint_4"), 1u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpULessThan %bool", "%uint_4"), 1u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpSelect %uint", "%uint_0"), 1u) << text;
|
||||
// ... and divided by the 2-word stride, with glslang's own sign conversion still downstream.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpUDiv %uint", "%uint_2"), 1u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpBitcast %int", ""), 1u) << text;
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheLengthOfABlockWithNoPrefixNeedsNoClamp) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data { dvec2 data[]; };
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = uint(data.length());
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
EXPECT_EQ(CountLinesWith(text, "OpArrayLength %uint", " 0"), 1u) << text;
|
||||
// Nothing to subtract, so nothing to clamp: the word count over the 4-word stride is it.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpISub", ""), 0u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpSelect", ""), 0u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpUDiv %uint", "%uint_4"), 1u) << text;
|
||||
}
|
||||
|
||||
// The graphics shape of the same CTS group: a fragment stage reading a `readonly` block. The
|
||||
// NonWritable the qualifier became is a promise about the whole block, and has to be on the one
|
||||
// member the flattened block keeps.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AReadOnlyOpenEndedBlockKeepsNonWritable) {
|
||||
const String source = R"(#version 450 core
|
||||
layout(binding = 4, std430) readonly buffer Buffer4 { dvec3 data[]; };
|
||||
layout(location = 0) out vec4 o_color;
|
||||
void main() {
|
||||
uint index = uint(gl_FragCoord.x);
|
||||
o_color = vec4(float(data[index].z));
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_FRAGMENT_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_EQ(CountLinesWith(Disassemble(input), "OpMemberDecorate %Buffer4 0 NonWritable", ""), 1u)
|
||||
<< Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = ExpectOpenEndedWordArray(output, "Buffer4");
|
||||
EXPECT_EQ(CountLinesWith(text, "OpMemberDecorate %Buffer4 0 NonWritable", ""), 1u) << text;
|
||||
// dvec3: stride 8 words, .z at +4.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_8"), 1u) << text;
|
||||
EXPECT_GE(CountLinesWith(text, "OpIAdd %uint", "%uint_4"), 1u) << text;
|
||||
}
|
||||
|
||||
// Writing through an open-ended block, which no CTS case does but any shader may: the store
|
||||
// is decomposed into the same words the load would have read, so the bytes the application
|
||||
// gets back are the ones GL says it wrote.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AnOpenEndedBlockIsWrittenThroughTheSameWords) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 1) buffer Data { double data[]; };
|
||||
void main() {
|
||||
data[gl_LocalInvocationID.x] = double(gl_LocalInvocationID.y);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// One dynamic index, scaled to the 2-word element ...
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_2"), 1u) << text;
|
||||
// ... and the double left as exactly two word stores, nothing wider.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpStore", ""), 2u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpAccessChain %_ptr_StorageBuffer_uint", ""), 2u) << text;
|
||||
}
|
||||
|
||||
// The exact compute shader KHR-Single-GL45.subgroups.arithmetic.compute.subgroupmul_double
|
||||
// generates, so the CTS shape is pinned as it is and not as a paraphrase of it.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheSubgroupMulDoubleComputeShaderIsFlattened) {
|
||||
const String source = R"(#version 450
|
||||
#extension GL_KHR_shader_subgroup_arithmetic: enable
|
||||
#extension GL_KHR_shader_subgroup_ballot: enable
|
||||
layout (local_size_x = 16, local_size_y = 1, local_size_z = 1) in;
|
||||
layout(binding = 0, std430) buffer Buffer0
|
||||
{
|
||||
uint result[];
|
||||
};
|
||||
layout(binding = 1, std430) buffer Buffer1
|
||||
{
|
||||
double data[];
|
||||
};
|
||||
|
||||
void main (void)
|
||||
{
|
||||
uvec3 globalSize = gl_NumWorkGroups * gl_WorkGroupSize;
|
||||
highp uint offset = globalSize.x * ((globalSize.y * gl_GlobalInvocationID.z) + gl_GlobalInvocationID.y) + gl_GlobalInvocationID.x;
|
||||
uvec4 mask = subgroupBallot(true);
|
||||
uint start = 0u, end = gl_SubgroupSize;
|
||||
double ref = double(1);
|
||||
uint tempResult = 0u;
|
||||
for (uint index = start; index < end; index++)
|
||||
{
|
||||
if (subgroupBallotBitExtract(mask, index))
|
||||
{
|
||||
ref = ref * data[index];
|
||||
}
|
||||
}
|
||||
tempResult = (abs(ref - subgroupMul(data[gl_SubgroupInvocationID])) < 0.00001) ? 0x1u : 0u;
|
||||
if (1u == (gl_SubgroupInvocationID % 2u))
|
||||
{
|
||||
mask = subgroupBallot(true);
|
||||
ref = double(1);
|
||||
for (uint index = start; index < end; index++)
|
||||
{
|
||||
if (subgroupBallotBitExtract(mask, index))
|
||||
{
|
||||
ref = ref * data[index];
|
||||
}
|
||||
}
|
||||
tempResult |= (abs(ref - subgroupMul(data[gl_SubgroupInvocationID])) < 0.00001) ? 0x2u : 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
tempResult |= 0x2u;
|
||||
}
|
||||
result[offset] = tempResult;
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Buffer1");
|
||||
// Four reads of the array, each scaled to the 2-word element.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_2"), 4u) << text;
|
||||
// The result block holds no double and is not the pass's business.
|
||||
const Uint32 resultStructId = StructIdNamed(output, "Buffer0");
|
||||
ASSERT_NE(resultStructId, 0u) << text;
|
||||
const Vector<Uint32> resultMembers = MemberTypesOf(output, resultStructId);
|
||||
ASSERT_EQ(resultMembers.size(), 1u);
|
||||
EXPECT_TRUE(IsUint32Type(output, RuntimeArrayElementOf(output, resultMembers[0]))) << text;
|
||||
}
|
||||
|
||||
// A runtime array whose element is a MATRIX. The member's own MatrixStride and RowMajor
|
||||
// decorations describe those elements, so a row-major one has to be declined - its columns are
|
||||
// not contiguous, and addressing it in column order against a row-major buffer would be silently
|
||||
// wrong bytes rather than a refusal. The column-major twin must flatten, stepping by the
|
||||
// element's stride and then by the column's.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, ARowMajorMatrixRuntimeArrayIsLeftToTheDemotion) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1, row_major) buffer Data { dmat4 data[]; };
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = uint(data[gl_LocalInvocationID.x][1][2]);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// The premise: glslang really did mark the member row-major.
|
||||
EXPECT_EQ(CountLinesWith(Disassemble(input), "OpMemberDecorate %Data 0 RowMajor", ""), 1u)
|
||||
<< Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = Disassemble(output);
|
||||
const Uint32 structId = StructIdNamed(output, "Data");
|
||||
ASSERT_NE(structId, 0u) << text;
|
||||
const Vector<Uint32> members = MemberTypesOf(output, structId);
|
||||
ASSERT_EQ(members.size(), 1u) << text;
|
||||
// Still a runtime array of matrices - narrowed to fp32 by the demotion, not re-addressed.
|
||||
EXPECT_NE(DecorationValueOf(output, members[0], kDecorationArrayStride), 4u)
|
||||
<< "a row-major matrix element must not have been flattened into words\n"
|
||||
<< text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", ""), 0u)
|
||||
<< "nothing should have been re-addressed\n"
|
||||
<< text;
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AColumnMajorMatrixRuntimeArrayStepsByItsColumnStride) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data { dmat2x4 data[]; };
|
||||
void main() {
|
||||
dvec4 column = data[gl_LocalInvocationID.x][1];
|
||||
result[gl_GlobalInvocationID.x] = uint(column.w);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// dmat2x4: two columns of dvec4, column stride 32 bytes, so one element is 64 bytes -
|
||||
// 16 words - and column 1 starts 8 words into it.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_16"), 1u) << text;
|
||||
// Exactly one +8: the column's own offset inside the element. A second would mean a word
|
||||
// past the column was being addressed off that same base.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", "%uint_8"), 1u) << text;
|
||||
// All eight words of that column are read - the last of its four doubles ends at +7 ...
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", "%uint_7"), 1u) << text;
|
||||
// ... and the column that was not asked for is not touched: nothing is read at +9 or past.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", "%uint_9"), 0u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIAdd %uint", "%uint_10"), 0u) << text;
|
||||
}
|
||||
|
||||
// A runtime array whose element is a STRUCT: the same walk, and the same decline test, as a
|
||||
// bounded array of them - a shape no other open-ended case reaches.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AStructRuntimeArrayStepsByItsElementStride) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
struct Pair { double a; float b; };
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data { Pair data[]; };
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = uint(data[gl_LocalInvocationID.x].a) +
|
||||
uint(data[gl_LocalInvocationID.x].b);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// std430 rounds `{ double a; float b; }` up to its 8-byte alignment: 16 bytes, 4 words,
|
||||
// with `b` two words in.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_4"), 2u) << text;
|
||||
EXPECT_GE(CountLinesWith(text, "OpIAdd %uint", "%uint_2"), 1u) << text;
|
||||
}
|
||||
|
||||
// The leaf cap bounds ONE load or store, not a member's size: a block whose element is far too
|
||||
// big to expand whole is still flattened while every access to it names a scalar. Declining it
|
||||
// would leave the application's 8-byte-stride doubles to the demotion's re-derived stride 4 -
|
||||
// the exact defect the open-ended shape exists to avoid.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AHugeRuntimeArrayElementIsStillFlattenedWhenAccessesAreSmall) {
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 16) in;
|
||||
struct Big { dvec4 v[300]; };
|
||||
layout(std430, binding = 0) buffer Sink { uint result[]; };
|
||||
layout(std430, binding = 1) buffer Data { Big data[]; };
|
||||
void main() {
|
||||
result[gl_GlobalInvocationID.x] = uint(data[gl_LocalInvocationID.x].v[3].y);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
// 300 dvec4 of 32 bytes each: 9600 bytes, 2400 words per element - 1200 scalars, well past
|
||||
// the per-access cap that a whole-element load would have to respect and this never does.
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", "%uint_2400"), 1u) << text;
|
||||
// v[3].y is 3 * 8 + 2 = 26 words into the element.
|
||||
EXPECT_GE(CountLinesWith(text, "OpIAdd %uint", "%uint_26"), 1u) << text;
|
||||
}
|
||||
|
||||
// The flatten preserves a byte layout ACROSS a narrowing; where the backend consumes 64-bit
|
||||
// floats itself there is nothing to preserve, and the open-ended block has to keep its runtime
|
||||
// array of doubles exactly as the driver would lay it out.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheNativePathLeavesAnOpenEndedBlockAndItsDoublesAlone) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, OpenEndedComputeSource("double"));
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String text = Disassemble(output);
|
||||
const Uint32 structId = StructIdNamed(output, "Data");
|
||||
ASSERT_NE(structId, 0u) << text;
|
||||
const Vector<Uint32> members = MemberTypesOf(output, structId);
|
||||
ASSERT_EQ(members.size(), 1u) << text;
|
||||
EXPECT_NE(RuntimeArrayElementOf(output, members[0]), 0u)
|
||||
<< "the member should still be a runtime array\n"
|
||||
<< text;
|
||||
EXPECT_EQ(DecorationValueOf(output, members[0], kDecorationArrayStride), 8u)
|
||||
<< "the array must keep the 8-byte stride the application bound\n"
|
||||
<< text;
|
||||
EXPECT_GT(CountFloatTypesOfWidth(output, 64), 0u)
|
||||
<< "nothing narrows here, so the doubles must survive\n"
|
||||
<< text;
|
||||
}
|
||||
|
||||
// The other backend prints the flattened module through SPIRV-Cross: an open-ended `uint[]`
|
||||
// member has to come out as ESSL that names no 64-bit type. The `.length()` shape is here too,
|
||||
// because the OpArrayLength the rewrite re-issues is the one instruction in it whose ESSL
|
||||
// spelling is not plain arithmetic - if that backend ever refused it on the flattened member,
|
||||
// a DirectGLES shader asking an fp64 buffer its length would fail at link and nowhere else.
|
||||
namespace {
|
||||
String TranspileToEssl(const Vector<Uint32>& spirv) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
EXPECT_EQ(session.CreateOptions(&options), SPVC_SUCCESS);
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
EXPECT_EQ(session.SetOptions(options), SPVC_SUCCESS);
|
||||
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? *essl : String{};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AnOpenEndedBlockCanBeEmittedAsEssl) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, OpenEndedComputeSource("dvec4"));
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
ExpectOpenEndedWordArray(output, "Data");
|
||||
|
||||
const String essl = TranspileToEssl(output);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("double"), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("dvec"), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("uint"), String::npos) << essl;
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheRewrittenLengthCanBeEmittedAsEssl) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kOpenEndedLengthSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
ExpectOpenEndedWordArray(output, "Data");
|
||||
|
||||
const String essl = TranspileToEssl(output);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("double"), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("dvec"), String::npos) << essl;
|
||||
// The length survived as a length - it was not folded away or dropped on the floor.
|
||||
EXPECT_NE(essl.find(".length()"), String::npos) << essl;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The gate from the other side: a runtime array anywhere but the block's own last member is a
|
||||
// shape GLSL cannot spell and this pass does not describe. SPIR-V can spell it, so both are
|
||||
// hand-written, and both are invalid Vulkan SPIR-V - the chain runs without its validator here,
|
||||
// which is also why neither can be a validation-failure count.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
// `buffer Odd { double data[]; uint tail; }`, the runtime array FIRST.
|
||||
const char* kRuntimeArrayNotLastAsm = R"(
|
||||
OpCapability Shader
|
||||
OpCapability Float64
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint GLCompute %main "main"
|
||||
OpExecutionMode %main LocalSize 1 1 1
|
||||
OpName %Odd "Odd"
|
||||
OpName %var ""
|
||||
OpDecorate %_runtimearr_double ArrayStride 8
|
||||
OpDecorate %Odd Block
|
||||
OpMemberDecorate %Odd 0 Offset 0
|
||||
OpMemberDecorate %Odd 1 Offset 8
|
||||
OpDecorate %var Binding 0
|
||||
OpDecorate %var DescriptorSet 0
|
||||
%void = OpTypeVoid
|
||||
%3 = OpTypeFunction %void
|
||||
%uint = OpTypeInt 32 0
|
||||
%int = OpTypeInt 32 1
|
||||
%int_0 = OpConstant %int 0
|
||||
%int_1 = OpConstant %int 1
|
||||
%double = OpTypeFloat 64
|
||||
%double_2 = OpConstant %double 2
|
||||
%_runtimearr_double = OpTypeRuntimeArray %double
|
||||
%Odd = OpTypeStruct %_runtimearr_double %uint
|
||||
%_ptr_StorageBuffer_Odd = OpTypePointer StorageBuffer %Odd
|
||||
%var = OpVariable %_ptr_StorageBuffer_Odd StorageBuffer
|
||||
%_ptr_StorageBuffer_double = OpTypePointer StorageBuffer %double
|
||||
%main = OpFunction %void None %3
|
||||
%5 = OpLabel
|
||||
%6 = OpAccessChain %_ptr_StorageBuffer_double %var %int_0 %int_1
|
||||
OpStore %6 %double_2
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
|
||||
// `struct Inner { double data[]; }; buffer Outer { uint head; Inner inner; }`: the runtime
|
||||
// array IS last, but of a member rather than of the block.
|
||||
const char* kRuntimeArrayNestedAsm = R"(
|
||||
OpCapability Shader
|
||||
OpCapability Float64
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint GLCompute %main "main"
|
||||
OpExecutionMode %main LocalSize 1 1 1
|
||||
OpName %Outer "Outer"
|
||||
OpName %Inner "Inner"
|
||||
OpName %var ""
|
||||
OpDecorate %_runtimearr_double ArrayStride 8
|
||||
OpMemberDecorate %Inner 0 Offset 0
|
||||
OpDecorate %Outer Block
|
||||
OpMemberDecorate %Outer 0 Offset 0
|
||||
OpMemberDecorate %Outer 1 Offset 8
|
||||
OpDecorate %var Binding 0
|
||||
OpDecorate %var DescriptorSet 0
|
||||
%void = OpTypeVoid
|
||||
%3 = OpTypeFunction %void
|
||||
%uint = OpTypeInt 32 0
|
||||
%int = OpTypeInt 32 1
|
||||
%int_0 = OpConstant %int 0
|
||||
%int_1 = OpConstant %int 1
|
||||
%double = OpTypeFloat 64
|
||||
%double_2 = OpConstant %double 2
|
||||
%_runtimearr_double = OpTypeRuntimeArray %double
|
||||
%Inner = OpTypeStruct %_runtimearr_double
|
||||
%Outer = OpTypeStruct %uint %Inner
|
||||
%_ptr_StorageBuffer_Outer = OpTypePointer StorageBuffer %Outer
|
||||
%var = OpVariable %_ptr_StorageBuffer_Outer StorageBuffer
|
||||
%_ptr_StorageBuffer_double = OpTypePointer StorageBuffer %double
|
||||
%main = OpFunction %void None %3
|
||||
%5 = OpLabel
|
||||
%6 = OpAccessChain %_ptr_StorageBuffer_double %var %int_1 %int_0 %int_1
|
||||
OpStore %6 %double_2
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
|
||||
Vector<Uint32> AssembleUnchecked(const char* asmText) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<Uint32> module;
|
||||
EXPECT_TRUE(tools.Assemble(asmText, &module));
|
||||
return module;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, ARuntimeArrayThatIsNotTheBlocksLastMemberIsLeftToTheDemotion) {
|
||||
struct Shape {
|
||||
const char* asmText;
|
||||
const char* blockName;
|
||||
};
|
||||
const Shape shapes[] = {{kRuntimeArrayNotLastAsm, "Odd"}, {kRuntimeArrayNestedAsm, "Outer"}};
|
||||
for (const Shape& shape : shapes) {
|
||||
SCOPED_TRACE(shape.blockName);
|
||||
const Vector<Uint32> input = AssembleUnchecked(shape.asmText);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, false, false));
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = Disassemble(output);
|
||||
|
||||
// Declined: both members are still there, and the demotion narrowed them the old way.
|
||||
const Uint32 structId = StructIdNamed(output, shape.blockName);
|
||||
ASSERT_NE(structId, 0u) << text;
|
||||
EXPECT_EQ(MemberTypesOf(output, structId).size(), 2u) << text;
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", ""), 0u)
|
||||
<< "nothing should have been re-addressed\n"
|
||||
<< text;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The front end declares types in first-use order, so a block that is the first thing the
|
||||
// shader touches is declared before the module's `uint` - and the flattened member is an array
|
||||
// OF `uint`. For an OPEN-ENDED block the pass moves that operand-less type up in front of the
|
||||
// block rather than declining, so that where a buffer of doubles stands in the shader does not
|
||||
// decide whether its bytes survive. A BOUNDED block in the same position keeps the decline it
|
||||
// has always had: widening that is a change to a path this fix does not need, and the pair below
|
||||
// pins both halves.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
// The position of <id>'s declaration in instruction order, or 0 when it has none.
|
||||
Uint32 DeclarationIndexOf(const Vector<Uint32>& spirv, Uint32 id) {
|
||||
Uint32 index = 0;
|
||||
Uint32 found = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
++index;
|
||||
if (found != 0 || wordCount < 2) return;
|
||||
// Every OpType* has its result id in word 1; that is all this is asked about.
|
||||
if (opcode >= kOpTypeInt && opcode <= kOpTypeStruct && words[1] == id) found = index;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
Uint32 Uint32TypeIdOf(const Vector<Uint32>& spirv) {
|
||||
Uint32 typeId = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpTypeInt && wordCount >= 4 && words[2] == 32u && words[3] == 0u) typeId = words[1];
|
||||
});
|
||||
return typeId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, AnOpenEndedBlockDeclaredBeforeTheModulesUintIsStillFlattened) {
|
||||
// The block is the first thing main touches, and nothing before it needs a uint - not even
|
||||
// an array length, which is a uint constant and would declare one.
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Data { double data[]; };
|
||||
layout(std430, binding = 1) buffer Sink { float result[]; };
|
||||
void main() {
|
||||
result[0] = float(data[0] + data[1]);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputStructId = StructIdNamed(input, "Data");
|
||||
ASSERT_NE(inputStructId, 0u);
|
||||
const Uint32 inputUintId = Uint32TypeIdOf(input);
|
||||
// The premise: the module's uint really is declared after the block (or not at all).
|
||||
ASSERT_TRUE(inputUintId == 0 ||
|
||||
DeclarationIndexOf(input, inputUintId) > DeclarationIndexOf(input, inputStructId))
|
||||
<< "this shader was meant to declare the block before any uint\n"
|
||||
<< Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = ExpectOpenEndedWordArray(output, "Data");
|
||||
const Uint32 structId = StructIdNamed(output, "Data");
|
||||
ASSERT_NE(structId, 0u) << text;
|
||||
const Vector<Uint32> members = MemberTypesOf(output, structId);
|
||||
ASSERT_EQ(members.size(), 1u) << text;
|
||||
// And the uint now stands in front of the block it is an element of.
|
||||
EXPECT_LT(DeclarationIndexOf(output, RuntimeArrayElementOf(output, members[0])),
|
||||
DeclarationIndexOf(output, structId))
|
||||
<< text;
|
||||
}
|
||||
|
||||
TEST_F(FlattenFloat64StorageBlockTest, ABoundedBlockDeclaredBeforeTheModulesUintIsLeftToTheDemotion) {
|
||||
// The same position, a bounded block: this is the shape that has always been declined, and
|
||||
// it stays declined - its members and the demotion's own repacking come through untouched.
|
||||
const String source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Wide {
|
||||
double data0;
|
||||
dvec2 data1;
|
||||
} g_wide;
|
||||
layout(std430, binding = 1) buffer Sink { float result[]; };
|
||||
void main() {
|
||||
double sum = g_wide.data0 + g_wide.data1.y;
|
||||
result[0] = float(sum);
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputStructId = StructIdNamed(input, "Wide");
|
||||
ASSERT_NE(inputStructId, 0u);
|
||||
const Uint32 inputUintId = Uint32TypeIdOf(input);
|
||||
ASSERT_TRUE(inputUintId == 0 ||
|
||||
DeclarationIndexOf(input, inputUintId) > DeclarationIndexOf(input, inputStructId))
|
||||
<< "this shader was meant to declare the block before any uint\n"
|
||||
<< Disassemble(input);
|
||||
|
||||
const Vector<Uint32> output = Sanitize(input);
|
||||
ASSERT_FALSE(output.empty());
|
||||
const String text = Disassemble(output);
|
||||
const Uint32 structId = StructIdNamed(output, "Wide");
|
||||
ASSERT_NE(structId, 0u) << text;
|
||||
EXPECT_EQ(MemberTypesOf(output, structId).size(), 2u)
|
||||
<< "a bounded block in this position must keep the decline it shipped with\n"
|
||||
<< text;
|
||||
EXPECT_EQ(CountLinesWith(text, "OpIMul %uint", ""), 0u)
|
||||
<< "nothing should have been re-addressed\n"
|
||||
<< text;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.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 <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
// How many times `needle` occurs in `haystack`.
|
||||
SizeT CountOf(const String& haystack, const String& needle) {
|
||||
SizeT count = 0;
|
||||
for (SizeT at = haystack.find(needle); at != String::npos; at = haystack.find(needle, at + 1)) {
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// The tessellation evaluation stage of
|
||||
// KHR-GLxx.shading_language_420pack.length_of_vector_and_matrix_*, reduced to what this
|
||||
// pass is about: one block consumed, one block produced, a plain varying in each
|
||||
// direction, and NO location written anywhere in the source. Every location in the
|
||||
// emitted ESSL is invented by glslang's cross-stage IO resolver.
|
||||
const char* kTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
in vec4 tcs_tes_result[];
|
||||
out vec4 tes_gs_result;
|
||||
|
||||
in TCSOutputBlock {
|
||||
vec4 tcs_tes_variable;
|
||||
} input_block[];
|
||||
out TESOutputBlock {
|
||||
vec4 tes_gs_variable;
|
||||
} output_block;
|
||||
|
||||
void main()
|
||||
{
|
||||
tes_gs_result = tcs_tes_result[0];
|
||||
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||
}
|
||||
)";
|
||||
|
||||
// The OTHER place a block's location can live. When the application locates the MEMBERS
|
||||
// rather than the block, glslang emits one OpMemberDecorate Location per member and
|
||||
// NOTHING on the variable - and SPIRV-Cross then suppresses the block-level qualifier and
|
||||
// prints the member ones instead. A strip that only looked at the variable would find
|
||||
// nothing to remove here, report "unchanged", and leave the emitted ESSL carrying exactly
|
||||
// the located block the driver drops the payload for.
|
||||
const char* kMemberLocatedTessEvalSource = R"(#version 450 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
in TCSOutputBlock {
|
||||
layout(location = 4) vec4 tcs_tes_variable;
|
||||
layout(location = 5) vec4 tcs_tes_second;
|
||||
} input_block[];
|
||||
out TESOutputBlock {
|
||||
layout(location = 6) vec4 tes_gs_variable;
|
||||
layout(location = 7) vec4 tes_gs_second;
|
||||
} output_block;
|
||||
|
||||
void main()
|
||||
{
|
||||
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||
output_block.tes_gs_second = input_block[0].tcs_tes_second;
|
||||
}
|
||||
)";
|
||||
|
||||
// A stage with no interface block at all: the pass must leave its located varyings alone
|
||||
// and report that it changed nothing, so the caller declines the re-serialised module.
|
||||
const char* kNoBlockTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
in vec4 tcs_tes_result[];
|
||||
out vec4 tes_gs_result;
|
||||
|
||||
void main()
|
||||
{
|
||||
tes_gs_result = tcs_tes_result[0];
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// NOTE ON spirv-val, because its absence here is deliberate and every sibling pass test
|
||||
// asserts the opposite. Vulkan SPIR-V REQUIRES a Location decoration on every user-defined
|
||||
// Input/Output variable ([VUID-StandaloneSpirv-Location-04915]), so a module whose interface
|
||||
// blocks have had theirs removed is INVALID Vulkan SPIR-V by construction - that is what the
|
||||
// pass was asked to produce. It never reaches a driver as SPIR-V: DirectGLES runs this last
|
||||
// in its chain and hands the result straight to SPIRV-Cross, which needs no location to print
|
||||
// a block. What the cases below assert instead is the thing that actually matters - that
|
||||
// SPIRV-Cross still emits a complete, matchable interface from it.
|
||||
class StripIoBlockLocationsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
TEST_F(StripIoBlockLocationsTest, DropsTheQualifierFromBothBlocksAndLeavesVaryingsAlone) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// The defect this exists for, pinned before the repair: SPIRV-Cross really does print a
|
||||
// location on the blocks, and on this driver that is what loses their payload.
|
||||
const String before = Transpile(input);
|
||||
EXPECT_NE(before.find(") in TCSOutputBlock"), String::npos) << before;
|
||||
EXPECT_NE(before.find(") out TESOutputBlock"), String::npos) << before;
|
||||
|
||||
bool strippedAny = false;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(strippedAny);
|
||||
|
||||
const String after = Transpile(output);
|
||||
// The blocks come out bare...
|
||||
EXPECT_NE(after.find("in TCSOutputBlock"), String::npos) << after;
|
||||
EXPECT_NE(after.find("out TESOutputBlock"), String::npos) << after;
|
||||
EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos)
|
||||
<< "the consumed block still carries a layout qualifier:\n"
|
||||
<< after;
|
||||
EXPECT_EQ(after.find(") out TESOutputBlock"), String::npos)
|
||||
<< "the produced block still carries a layout qualifier:\n"
|
||||
<< after;
|
||||
// ...and everything ES matches them by is untouched, which is what makes the unlocated
|
||||
// interface still find its other end.
|
||||
EXPECT_NE(after.find("input_block"), String::npos) << after;
|
||||
EXPECT_NE(after.find("output_block"), String::npos) << after;
|
||||
EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after;
|
||||
EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after;
|
||||
// The PLAIN varyings keep their locations. They work on the affected driver, and a
|
||||
// fragment stage's inputs and a vertex stage's attributes are matched by them.
|
||||
EXPECT_NE(after.find("in vec4 tcs_tes_result"), String::npos) << after;
|
||||
EXPECT_NE(after.find("out vec4 tes_gs_result"), String::npos) << after;
|
||||
EXPECT_EQ(CountOf(after, "layout(location"), 2u)
|
||||
<< "exactly the two plain varyings should still be located:\n"
|
||||
<< after;
|
||||
}
|
||||
|
||||
TEST_F(StripIoBlockLocationsTest, StripsOnlyTheDirectionTheCallerArmed) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// A separate-shader-objects program that ENDS at this stage: the block it produces is
|
||||
// matched, in another program that never saw this decision, by the location alone. Only
|
||||
// the consumed side may lose its qualifier.
|
||||
bool strippedAny = false;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, false, strippedAny, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(strippedAny);
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos) << after;
|
||||
EXPECT_NE(after.find(") out TESOutputBlock"), String::npos)
|
||||
<< "the produced block's location was dropped even though its consumer is elsewhere:\n"
|
||||
<< after;
|
||||
|
||||
// And the mirror image, for a program that BEGINS at this stage.
|
||||
bool strippedOutputOnly = false;
|
||||
Vector<Uint32> outputOnly;
|
||||
ASSERT_TRUE(
|
||||
ShaderCompiler::StripIoBlockLocationsForEssl(input, false, true, strippedOutputOnly, outputOnly, true));
|
||||
ASSERT_FALSE(outputOnly.empty());
|
||||
EXPECT_TRUE(strippedOutputOnly);
|
||||
const String afterOutputOnly = Transpile(outputOnly);
|
||||
EXPECT_NE(afterOutputOnly.find(") in TCSOutputBlock"), String::npos) << afterOutputOnly;
|
||||
EXPECT_EQ(afterOutputOnly.find(") out TESOutputBlock"), String::npos) << afterOutputOnly;
|
||||
}
|
||||
|
||||
// The regression guard for the shape a variable-only strip walks straight past.
|
||||
TEST_F(StripIoBlockLocationsTest, DropsLocationsTheApplicationPutOnTheBlockMembers) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kMemberLocatedTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// The defect, pinned first: SPIRV-Cross prints the member locations, and there is no
|
||||
// block-level qualifier for a variable-level strip to find.
|
||||
const String before = Transpile(input);
|
||||
EXPECT_NE(before.find("layout(location = 4)"), String::npos) << before;
|
||||
EXPECT_NE(before.find("layout(location = 6)"), String::npos) << before;
|
||||
|
||||
bool strippedAny = false;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(strippedAny) << "the member-located block was passed by, and reporting no change "
|
||||
"makes the caller decline the module and say nothing about it";
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_EQ(CountOf(after, "layout(location"), 0u)
|
||||
<< "a member location survived, so the emitted block is still the shape the driver "
|
||||
"drops the payload for:\n"
|
||||
<< after;
|
||||
// The interface still has to be matchable: same blocks, same members, same order.
|
||||
EXPECT_NE(after.find("TCSOutputBlock"), String::npos) << after;
|
||||
EXPECT_NE(after.find("TESOutputBlock"), String::npos) << after;
|
||||
EXPECT_LT(after.find("tcs_tes_variable"), after.find("tcs_tes_second")) << after;
|
||||
EXPECT_LT(after.find("tes_gs_variable"), after.find("tes_gs_second")) << after;
|
||||
}
|
||||
|
||||
// ...and the same shape with only ONE direction armed. The member decorations belong to the
|
||||
// TYPE, so the unarmed block's must survive - it is matched, in another program, by exactly
|
||||
// those numbers.
|
||||
TEST_F(StripIoBlockLocationsTest, KeepsMemberLocationsOnTheDirectionTheCallerDidNotArm) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kMemberLocatedTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
bool strippedAny = false;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, false, strippedAny, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(strippedAny);
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_EQ(after.find("layout(location = 4)"), String::npos) << after;
|
||||
EXPECT_EQ(after.find("layout(location = 5)"), String::npos) << after;
|
||||
EXPECT_NE(after.find("layout(location = 6)"), String::npos)
|
||||
<< "the produced block lost its member locations even though its consumer is elsewhere:\n"
|
||||
<< after;
|
||||
EXPECT_NE(after.find("layout(location = 7)"), String::npos) << after;
|
||||
}
|
||||
|
||||
TEST_F(StripIoBlockLocationsTest, ReportsNoChangeForAStageWithoutInterfaceBlocks) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kNoBlockTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const String before = Transpile(input);
|
||||
|
||||
bool strippedAny = true; // deliberately wrong going in; the pass must clear it
|
||||
Vector<Uint32> output;
|
||||
ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true);
|
||||
EXPECT_FALSE(strippedAny) << "a stage with no interface block must report nothing stripped, or "
|
||||
"the caller adopts a re-serialised module for nothing";
|
||||
// gl_PerVertex is an Input AND an Output block in this stage and must not be touched; the
|
||||
// located plain varyings must not be either. Either way the emitted ESSL is unchanged.
|
||||
if (!output.empty()) {
|
||||
EXPECT_EQ(Transpile(output), before);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(StripIoBlockLocationsTest, DeclinesWhenNeitherDirectionIsArmed) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
bool strippedAny = true;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::StripIoBlockLocationsForEssl(input, false, false, strippedAny, output, true));
|
||||
EXPECT_FALSE(strippedAny);
|
||||
EXPECT_TRUE(output.empty()) << "an unarmed call must not even re-serialise the module";
|
||||
}
|
||||
@@ -499,6 +499,19 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
|
||||
v.nativeFloat64 = true;
|
||||
variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
{ // CompileEnv::DemotesTessellationPointSize(): phase B rewrites the cached modules
|
||||
// under it (the point-size demotion), so one key shape would describe two module
|
||||
// sets - built-in kept vs carried as a varying with the capability stripped.
|
||||
SpirvTranslationKeyInputs v = base;
|
||||
v.demoteTessellationPointSize = true;
|
||||
variants.emplace_back("demoteTessellationPointSize", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
{ // ... and its geometry twin, keyed separately because the ES loader really does
|
||||
// probe the two extension families independently.
|
||||
SpirvTranslationKeyInputs v = base;
|
||||
v.demoteGeometryPointSize = true;
|
||||
variants.emplace_back("demoteGeometryPointSize", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
// ---- inputs the WIDENED payload pulled into the key ----
|
||||
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
|
||||
// payload now carries, so they have to split the key. This is the group that would go
|
||||
@@ -651,6 +664,48 @@ TEST_F(TranslationCacheTest, NativeFloat64IsOutOfTheFrontendFingerprintAndInside
|
||||
<< "one L1 entry would then describe two different module sets";
|
||||
}
|
||||
|
||||
// The second and third capability bits under the same placement rule as nativeFloat64:
|
||||
// out of the front-end fingerprint (glslang produces the same thing either way), inside
|
||||
// the L1 key (phase B's point-size demotion rewrites the cached modules under them). The
|
||||
// accessor direction is pinned too, because it is INVERTED relative to the params field
|
||||
// and a swap of the arms would disable the device repair with every rendering test green.
|
||||
TEST_F(TranslationCacheTest, PointSizeDemotionBitsAreOutOfTheFrontendFingerprintAndInsideTheL1Key) {
|
||||
CompileEnv none; // no backend at all: never demote, standalone compiles stay standard
|
||||
CompileEnv hosting; // a backend that hosts the built-in
|
||||
CompileEnv demoting; // a backend that cannot
|
||||
hosting.backend = BackendType::DirectVulkan;
|
||||
demoting.backend = BackendType::DirectVulkan;
|
||||
demoting.params.SupportsTessellationPointSize = false;
|
||||
demoting.params.SupportsGeometryPointSize = false;
|
||||
|
||||
EXPECT_FALSE(none.DemotesTessellationPointSize());
|
||||
EXPECT_FALSE(none.DemotesGeometryPointSize());
|
||||
EXPECT_FALSE(hosting.DemotesTessellationPointSize());
|
||||
EXPECT_FALSE(hosting.DemotesGeometryPointSize());
|
||||
EXPECT_TRUE(demoting.DemotesTessellationPointSize());
|
||||
EXPECT_TRUE(demoting.DemotesGeometryPointSize());
|
||||
|
||||
EXPECT_EQ(ComputeFrontendCompileEnvFingerprint(hosting), ComputeFrontendCompileEnvFingerprint(demoting))
|
||||
<< "the point-size capability leaked into the front-end fingerprint";
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(hosting), ComputeCompileEnvFingerprint(demoting))
|
||||
<< "the whole-environment fingerprint has to notice it - it is a DynamicBackendParameters "
|
||||
"field, hashed by object representation";
|
||||
|
||||
const Vector<SpirvTranslationKeyInputs::Stage> stages{{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||
SpirvTranslationKeyInputs demotedKey = BaselineSpirvInputs(stages);
|
||||
demotedKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(demoting);
|
||||
demotedKey.demoteTessellationPointSize = demoting.DemotesTessellationPointSize();
|
||||
demotedKey.demoteGeometryPointSize = demoting.DemotesGeometryPointSize();
|
||||
SpirvTranslationKeyInputs keptKey = BaselineSpirvInputs(stages);
|
||||
keptKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(hosting);
|
||||
keptKey.demoteTessellationPointSize = hosting.DemotesTessellationPointSize();
|
||||
keptKey.demoteGeometryPointSize = hosting.DemotesGeometryPointSize();
|
||||
|
||||
EXPECT_FALSE(BuildSpirvTranslationKey(demotedKey) == BuildSpirvTranslationKey(keptKey))
|
||||
<< "one L1 entry would then describe two different module sets";
|
||||
}
|
||||
|
||||
// The other direction, one case per input that was KEPT. Each is a limit the front end
|
||||
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
|
||||
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
|
||||
@@ -1001,6 +1056,20 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
v.outputBlockRenames = &otherIoBlockRenames;
|
||||
variants.emplace_back("outputBlockRenames(other target)", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // the two arguments to StripIoBlockLocationsForEssl, and separate cases for the same
|
||||
// reason the rename maps are: a stage strips the blocks it CONSUMES only when the
|
||||
// producer is in this program and the ones it PRODUCES only when the consumer is, so
|
||||
// the two directions are independently armed and a key that folded them together
|
||||
// would serve a fragment stage's ESSL to a vertex stage that needs the opposite.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.stripInputBlockLocations = true;
|
||||
variants.emplace_back("stripInputBlockLocations", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.stripOutputBlockLocations = true;
|
||||
variants.emplace_back("stripOutputBlockLocations", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.enableSpirvValidation = true;
|
||||
|
||||
@@ -2052,6 +2052,81 @@ TEST_F(TextureTest, GetTextureSubImageRejectsPartialReadbackForNow) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// A cube map keeps each face as its own stored image, so a level's texel size reads z = 1 whichever
|
||||
// face is asked - but GL 4.6 core 8.11.4 addresses the six faces through zoffset, which is the
|
||||
// by-name spelling of the face token glGetTexImage takes. Both halves of that were missing: the z
|
||||
// range was measured against the level's 1, so every face but +X came back INVALID_OPERATION as a
|
||||
// partial read, and the destination-size check summed all six faces, so even face +X could not be
|
||||
// read into the one face's worth of buffer a single-face read has any reason to pass.
|
||||
TEST_F(TextureTest, GetTextureSubImageSelectsTheCubeFaceZOffsetNames) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
||||
// Every face carries its own index in the red channel, so a read that answers the wrong face
|
||||
// says which one it answered with.
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const Uint8 pixel[] = {static_cast<Uint8>(10 + face), 20, 30, 40};
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, 1, 1, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, pixel);
|
||||
}
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "seeding the six faces failed";
|
||||
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
Uint8 output[4] = {};
|
||||
MG_Impl::GLImpl::GetTextureSubImage(texture, 0, 0, 0, face, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
sizeof(output), output);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "reading face " << face << " errored";
|
||||
EXPECT_EQ(static_cast<int>(output[0]), 10 + face)
|
||||
<< "zoffset " << face << " answered with face " << (static_cast<int>(output[0]) - 10);
|
||||
}
|
||||
|
||||
// Past the last face. Still a partial read of a level with no sixth-and-beyond image.
|
||||
Uint8 output[4] = {};
|
||||
MG_Impl::GLImpl::GetTextureSubImage(texture, 0, 0, 0, 6, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, sizeof(output),
|
||||
output);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// glGetTexImage of ONE cube face packs one face, so a PIXEL_PACK_BUFFER holding one face is
|
||||
// exactly the right size for it. The validator used to measure the bound PBO against all SIX
|
||||
// faces' worth and refuse - INVALID_OPERATION for a buffer the copy that follows would have filled
|
||||
// precisely. glGetTexImage passes no bufSize, which skips the destination-size branch but NOT the
|
||||
// PBO one, so this is the only spelling where the six-face sizing was reachable at all.
|
||||
TEST_F(TextureTest, GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer) {
|
||||
constexpr GLsizei kEdge = 2;
|
||||
constexpr SizeT kFaceBytes = static_cast<SizeT>(kEdge) * kEdge * 4;
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
||||
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kEdge, kEdge);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
Uint8 seed[kFaceBytes];
|
||||
for (SizeT i = 0; i < kFaceBytes; ++i) seed[i] = static_cast<Uint8>(10 + face);
|
||||
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 0, 0, kEdge, kEdge, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, seed);
|
||||
}
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "seeding the six faces failed";
|
||||
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_PACK_BUFFER, buffer);
|
||||
MG_Impl::GLImpl::BufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(kFaceBytes), nullptr, GL_STREAM_READ);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "creating the one-face pixel pack buffer failed";
|
||||
|
||||
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< "a pixel pack buffer sized for the one face this call packs was refused";
|
||||
|
||||
Uint8 packed[kFaceBytes] = {};
|
||||
MG_Impl::GLImpl::GetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(kFaceBytes), packed);
|
||||
EXPECT_EQ(static_cast<int>(packed[0]), 15) << "the PBO holds face " << (static_cast<int>(packed[0]) - 10)
|
||||
<< ", not -Z";
|
||||
|
||||
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, TextureParameteriAndBindTextureUnitAreDirectStateAccess) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
@@ -3313,6 +3388,46 @@ TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFo
|
||||
}
|
||||
}
|
||||
|
||||
// The packed16 field-order quirk (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the
|
||||
// driver stores some packed16 allocations with a mirrored field order (the Mali defect
|
||||
// behind the KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* failures), the
|
||||
// three ES narrow formats move to 8-bit-per-channel storage. The transfer pair must NOT move
|
||||
// with the bit - it is already the UNorm8 component layout the canonical shadow holds - and
|
||||
// no other format may move with it either.
|
||||
TEST_F(TextureTest, NormalizePixelFormatWidensThePacked16FormatsUnderTheQuirkBit) {
|
||||
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
|
||||
struct {
|
||||
GLenum requested;
|
||||
GLenum expectedNarrow;
|
||||
GLenum expectedWidened;
|
||||
GLenum expectedFormat;
|
||||
} cases[] = {
|
||||
{GL_RGB565, GL_RGB565, GL_RGB8, GL_RGB},
|
||||
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA8, GL_RGBA},
|
||||
{GL_RGBA4, GL_RGBA4, GL_RGBA8, GL_RGBA},
|
||||
// Negative controls: a 32-bit packed format and an already-8-bit one stay put with
|
||||
// the bit set - the quirk is about 16-bit packed normalized storage and nothing else.
|
||||
{GL_RGB10_A2, GL_RGB10_A2, GL_RGB10_A2, GL_RGBA},
|
||||
{GL_RGBA8, GL_RGBA8, GL_RGBA8, GL_RGBA},
|
||||
};
|
||||
for (const auto& c : cases) {
|
||||
GLenum narrowInternal = 0, narrowFormat = 0, narrowType = 0;
|
||||
NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::None, &narrowInternal, &narrowFormat,
|
||||
&narrowType);
|
||||
EXPECT_EQ(narrowInternal, c.expectedNarrow) << "internalformat 0x" << std::hex << c.requested;
|
||||
|
||||
GLenum widenedInternal = 0, widenedFormat = 0, widenedType = 0;
|
||||
NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::WidenPacked16Norm, &widenedInternal,
|
||||
&widenedFormat, &widenedType);
|
||||
EXPECT_EQ(widenedInternal, c.expectedWidened) << "internalformat 0x" << std::hex << c.requested;
|
||||
// The transfer pair is identical narrow and widened: the widening changes only the ES
|
||||
// storage, never how client data is described to it.
|
||||
EXPECT_EQ(widenedFormat, narrowFormat) << "internalformat 0x" << std::hex << c.requested;
|
||||
EXPECT_EQ(widenedType, narrowType) << "internalformat 0x" << std::hex << c.requested;
|
||||
EXPECT_EQ(widenedFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.requested;
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip
|
||||
// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with
|
||||
// GL_RGB565 was rejected as an unknown internal format.
|
||||
|
||||
@@ -505,4 +505,47 @@ namespace {
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
|
||||
}
|
||||
|
||||
// ======================= which of the owner's layers a face names =======================
|
||||
|
||||
// A GL_TEXTURE_CUBE_MAP view over a LAYERED owner - a 2D array here, a cube-map ARRAY behaves
|
||||
// identically - is the one shape where the face a target names cannot be carried by the choice
|
||||
// of blob: the owner keeps every layer in ONE blob, so there is nothing for
|
||||
// ToOwnerUploadTarget to choose between and the face has to land in the byte offset instead.
|
||||
// It did not. The offset shifted by the view's layer origin alone, so all six face tokens read
|
||||
// the view's FIRST layer-face - silently, with real texels from a real layer, on every path
|
||||
// that answers out of the CPU shadow.
|
||||
//
|
||||
// The shadow is exactly what this exercises: the fixture's backend is not DirectVulkan, so the
|
||||
// by-name readback takes the shadow arm rather than asking a backend. (DirectVulkan's own path
|
||||
// resolves the face into a Vulkan baseArrayLayer and was always right, which is what made this
|
||||
// a disagreement between the two backends rather than a uniform wrong answer.)
|
||||
TEST_F(TextureViewTest, CubeMapViewOfAnArrayReadsTheFaceEachTokenNames) {
|
||||
constexpr GLint kLayers = 8;
|
||||
constexpr GLint kViewMinLayer = 2;
|
||||
|
||||
const GLuint storage = MakeImmutable2DArray(1, 1, kLayers);
|
||||
// Every layer carries its own index, so a read that lands on the wrong one says which one
|
||||
// answered instead of merely failing.
|
||||
for (GLint layer = 0; layer < kLayers; ++layer) {
|
||||
const Uint8 texel[] = {static_cast<Uint8>(10 + layer), 20, 30, 40};
|
||||
MG_Impl::GLImpl::TexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texel);
|
||||
}
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, kViewMinLayer, 6);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the cube-map view over the array was refused";
|
||||
|
||||
for (GLint face = 0; face < 6; ++face) {
|
||||
Uint8 output[4] = {};
|
||||
MG_Impl::GLImpl::GetTextureSubImage(view, 0, 0, 0, face, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
sizeof(output), output);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "reading face " << face << " errored";
|
||||
EXPECT_EQ(static_cast<GLint>(output[0]), 10 + kViewMinLayer + face)
|
||||
<< "face " << face << " of a view based at layer " << kViewMinLayer << " answered with layer "
|
||||
<< (static_cast<GLint>(output[0]) - 10);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -441,7 +441,7 @@ TEST(JobNodeException, AnExceptionEscapingABodyCancelsTheJobInsteadOfTerminating
|
||||
EXPECT_TRUE(job->IsCancelled());
|
||||
EXPECT_FALSE(job->IsComplete());
|
||||
ASSERT_EQ(job->diagnostics.logLines.size(), 1u);
|
||||
EXPECT_NE(job->diagnostics.logLines[0].find("boom"), String::npos);
|
||||
EXPECT_NE(job->diagnostics.logLines[0].text.find("boom"), String::npos);
|
||||
}
|
||||
|
||||
TEST(JobNodeException, ANonStandardExceptionIsContainedToo) {
|
||||
|
||||
@@ -95,11 +95,13 @@ namespace MobileGL::MG_Util::Async {
|
||||
// which means std::terminate for the whole process. Every job boundary contains
|
||||
// it and reports the job as Cancelled; the joining GL thread then sees a node
|
||||
// that produced no result, which is the same shape as an abandoned node.
|
||||
diagnostics.logLines.push_back(std::format("Job body threw: {}", e.what()));
|
||||
diagnostics.logLines.push_back(
|
||||
{MOBILEGL_LOG_LEVEL_DEBUG, std::format("Job body threw: {}", e.what())});
|
||||
TryTransition(JobState::Running, JobState::Cancelled);
|
||||
return;
|
||||
} catch (...) {
|
||||
diagnostics.logLines.emplace_back("Job body threw a non-std exception");
|
||||
diagnostics.logLines.push_back(
|
||||
{MOBILEGL_LOG_LEVEL_DEBUG, String("Job body threw a non-std exception")});
|
||||
TryTransition(JobState::Running, JobState::Cancelled);
|
||||
return;
|
||||
}
|
||||
@@ -162,10 +164,27 @@ namespace MobileGL::MG_Util::Async {
|
||||
"being written");
|
||||
|
||||
if (!node.diagnostics.logLines.empty()) {
|
||||
Vector<String> lines;
|
||||
Vector<DeferredLogLine> lines;
|
||||
lines.swap(node.diagnostics.logLines);
|
||||
for (const String& line : lines) {
|
||||
MGLOG_D("%s", line.c_str());
|
||||
for (const DeferredLogLine& line : lines) {
|
||||
// Per-line severity, because a shipped build compiles MGLOG_D away entirely
|
||||
// and a verdict that only this channel records would vanish with it. The
|
||||
// levels are the compile-time constants, so a suppressed one costs nothing
|
||||
// beyond the string the worker already built.
|
||||
switch (line.level) {
|
||||
case MOBILEGL_LOG_LEVEL_INFO:
|
||||
MGLOG_I("%s", line.text.c_str());
|
||||
break;
|
||||
case MOBILEGL_LOG_LEVEL_WARN:
|
||||
MGLOG_W("%s", line.text.c_str());
|
||||
break;
|
||||
case MOBILEGL_LOG_LEVEL_ERROR:
|
||||
MGLOG_E("%s", line.text.c_str());
|
||||
break;
|
||||
default:
|
||||
MGLOG_D("%s", line.text.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorCode.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
|
||||
@@ -35,9 +36,25 @@ namespace MobileGL::MG_Util::Async {
|
||||
UniquePtr<ErrorInfo> info;
|
||||
};
|
||||
|
||||
// One line of worker-side MGLOG text, with the severity the join replays it at.
|
||||
//
|
||||
// DEBUG is the default and stays the default: nearly every deferred line is per-program
|
||||
// trace that a shipped build compiles out, which is the whole reason this channel could
|
||||
// be a plain string vector for as long as it was. A line a SHIPPED build has to show -
|
||||
// the reason a repair refused, which no other surface records - has to name its level
|
||||
// here, or it is formatted on the worker and then thrown away at replay under the INFO
|
||||
// level every device and CI build pins. Callers that sit on a repeated path latch at
|
||||
// the SOURCE (a per-call-site atomic, exactly what MGLOG_*_ONCE does): the replay below
|
||||
// is one shared site for every job in the tree, so a latch there would silence
|
||||
// unrelated lines.
|
||||
struct DeferredLogLine {
|
||||
Int level = MOBILEGL_LOG_LEVEL_DEBUG;
|
||||
String text;
|
||||
};
|
||||
|
||||
struct JobDiagnostics {
|
||||
Vector<DeferredError> errors; // replayed, in ascending `sequence`, by the join
|
||||
Vector<String> logLines; // worker-side MGLOG text, flushed in order by the join
|
||||
Vector<DeferredError> errors; // replayed, in ascending `sequence`, by the join
|
||||
Vector<DeferredLogLine> logLines; // worker-side MGLOG text, flushed in order by the join
|
||||
};
|
||||
|
||||
// The scheduling primitive every asynchronous compile and link is built on. A node owns
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
namespace MobileGL::MG_Util::BackendLoader {
|
||||
static Bool UseAngle() {
|
||||
return MG_Config::Features.UseAngle;
|
||||
return MG_Config::Features.EsprytUseAngle;
|
||||
}
|
||||
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
|
||||
@@ -984,6 +984,20 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
|
||||
caps.SupportsViewportArray = true;
|
||||
}
|
||||
// EXT wins where both are advertised: it is the spelling the Android Extension
|
||||
// Pack mandates, so it is the one a driver is most likely to have tested.
|
||||
if (std::strcmp(extension, "GL_EXT_tessellation_point_size") == 0) {
|
||||
caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
|
||||
} else if (std::strcmp(extension, "GL_OES_tessellation_point_size") == 0 &&
|
||||
caps.TessellationPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
|
||||
caps.TessellationPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_geometry_point_size") == 0) {
|
||||
caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT;
|
||||
} else if (std::strcmp(extension, "GL_OES_geometry_point_size") == 0 &&
|
||||
caps.GeometryPointSizeSupport == MG_External::GLESCapabilities::PointSizeTier::None) {
|
||||
caps.GeometryPointSizeSupport = MG_External::GLESCapabilities::PointSizeTier::ExtensionOES;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The pointer check on top of the extension check makes each flag sufficient on its own
|
||||
@@ -1055,6 +1069,19 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
|
||||
MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
|
||||
caps.SupportsViewportArray ? "yes" : "no");
|
||||
{
|
||||
const auto pointSizeTierName = [](MG_External::GLESCapabilities::PointSizeTier tier) {
|
||||
switch (tier) {
|
||||
case MG_External::GLESCapabilities::PointSizeTier::ExtensionEXT: return "EXT";
|
||||
case MG_External::GLESCapabilities::PointSizeTier::ExtensionOES: return "OES";
|
||||
default: return "no";
|
||||
}
|
||||
};
|
||||
MGLOG_I(" tessellation gl_PointSize (EXT/OES_tessellation_point_size): %s",
|
||||
pointSizeTierName(caps.TessellationPointSizeSupport));
|
||||
MGLOG_I(" geometry gl_PointSize (EXT/OES_geometry_point_size): %s",
|
||||
pointSizeTierName(caps.GeometryPointSizeSupport));
|
||||
}
|
||||
|
||||
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
|
||||
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
|
||||
@@ -1697,13 +1724,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
|
||||
|
||||
// ForceOn means "emit the blocks unlocated", i.e. treat the driver as NOT supporting
|
||||
// located blocks - which is why the override reads inverted here. Auto is the probe's
|
||||
// own answer and is what every real run uses; the two forced settings exist so the
|
||||
// emulation can be exercised on a healthy driver (the integration lane) and turned
|
||||
// off again as a negative control.
|
||||
switch (MG_Config::Features.EsprytUnlocatedIoBlocks) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
caps.SupportsLocatedInterStageIoBlocks = false;
|
||||
MGLOG_I(" Located inter-stage interface blocks: forced OFF by "
|
||||
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed");
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
caps.SupportsLocatedInterStageIoBlocks = true;
|
||||
MGLOG_I(" Located inter-stage interface blocks: forced ON by "
|
||||
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed");
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
default:
|
||||
// SelfTest::ProbeLocatedIoBlocksLosePayload - 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.
|
||||
// Probed with its own controls rather than matched on a renderer string; see
|
||||
// DriverBugProbes.h for the shape and for why the two controls decide what the
|
||||
// finding is allowed to claim.
|
||||
caps.SupportsLocatedInterStageIoBlocks =
|
||||
!SelfTest::LocatedIoBlocksLosePayload(glesFuncs).detected;
|
||||
break;
|
||||
}
|
||||
MGLOG_I(" Located inter-stage interface blocks transport their payload: %s",
|
||||
caps.SupportsLocatedInterStageIoBlocks
|
||||
? "true"
|
||||
: "false (DirectGLES will emit tessellation/geometry programs' interface "
|
||||
"blocks without a location qualifier)");
|
||||
|
||||
caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
|
||||
caps.IsAngleLlvmpipeRenderer =
|
||||
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
||||
caps.AvoidSamplerMipmapMinFilter =
|
||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidSamplerMipmapMinFilter;
|
||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.EsprytAvoidSamplerMipmapMinFilter;
|
||||
caps.AvoidExplicitLodBias =
|
||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidExplicitLodBias;
|
||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.EsprytAvoidExplicitLodBias;
|
||||
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
|
||||
caps.SupportsDisjointTimerQuery ? "true" : "false");
|
||||
MGLOG_I(" GL_KHR_parallel_shader_compile supported: %s",
|
||||
|
||||
@@ -1118,6 +1118,23 @@ namespace MobileGL {
|
||||
ExtensionOES, // GL_OES_texture_buffer; ESSL below 320 must say GL_OES_texture_buffer
|
||||
};
|
||||
TextureBufferTier TextureBufferSupport = TextureBufferTier::None;
|
||||
// Which spelling of per-vertex point size a NON-VERTEX stage has, if any. In desktop
|
||||
// GL gl_PointSize is an ordinary gl_PerVertex member that any vertex-processing stage
|
||||
// may write and any program may capture by name; in ESSL it does not EXIST in a
|
||||
// tessellation or geometry stage until GL_EXT/OES_tessellation_point_size (resp.
|
||||
// ..._geometry_point_size) is requested - not even at 320, where the stages
|
||||
// themselves are core. SPIRV-Cross prints the identifier bare and asks for nothing,
|
||||
// exactly as it does for gl_ViewportIndex, so the directive has to be inserted into
|
||||
// the emitted source (RequestPointSizeExtension) and a driver with neither spelling
|
||||
// cannot compile such a stage at all. Extension string only: these add no entry
|
||||
// points, so there is no pointer to require.
|
||||
enum class PointSizeTier : Uint8 {
|
||||
None = 0, // neither spelling; the stage cannot name gl_PointSize
|
||||
ExtensionEXT, // GL_EXT_tessellation_point_size / GL_EXT_geometry_point_size
|
||||
ExtensionOES, // GL_OES_tessellation_point_size / GL_OES_geometry_point_size
|
||||
};
|
||||
PointSizeTier TessellationPointSizeSupport = PointSizeTier::None;
|
||||
PointSizeTier GeometryPointSizeSupport = PointSizeTier::None;
|
||||
// GL_MAX_TEXTURE_BUFFER_SIZE actually came back from the driver. False means the value
|
||||
// below is MobileGL's own floor, not a driver answer: the pname is only legal once
|
||||
// buffer textures exist, and querying it on a driver without them raises
|
||||
@@ -1218,10 +1235,10 @@ namespace MobileGL {
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
Bool IsAngleLlvmpipeRenderer = false;
|
||||
// IsAngleLlvmpipeRenderer combined with the
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle:
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle:
|
||||
// sampler min filters should drop their mipmap component.
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_AVOID_EXPLICIT_LOD_BIAS
|
||||
// IsAngleLlvmpipeRenderer combined with the MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS
|
||||
// feature toggle: LOD-bias emulation should not touch explicit-LOD lookups.
|
||||
Bool AvoidExplicitLodBias = false;
|
||||
// True when indirect draws leak the command's baseInstance word ("reserved,
|
||||
@@ -1230,6 +1247,17 @@ namespace MobileGL {
|
||||
// straight to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V
|
||||
// InstanceIndex, which includes firstInstance.
|
||||
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
// True when an inter-stage interface BLOCK carrying an explicit layout(location=)
|
||||
// actually delivers its payload across a tessellation or geometry boundary. The
|
||||
// Mali-G1-Ultra ES driver links such a program with an empty info log and then
|
||||
// hands the consuming stage zeroes; DirectGLES answers by emitting those blocks
|
||||
// with no location qualifier at all (StripIoBlockLocationsPass), which ES matches
|
||||
// by block name and member sequence instead.
|
||||
//
|
||||
// Defaults TRUE and stays true when the probe cannot run, because that is the
|
||||
// behaviour every driver had before the probe existed - a capability like this
|
||||
// must never be assumed broken on a driver nobody measured.
|
||||
Bool SupportsLocatedInterStageIoBlocks = true;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
// Its storage-buffer counterpart, queried separately because it is a separate limit:
|
||||
// Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Loader.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
@@ -91,7 +92,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
}
|
||||
|
||||
Bool IsShaderSubgroupForcedDisabled() {
|
||||
return MG_Config::Features.DisableSubgroup;
|
||||
return MG_Config::Features.MagmaDisableSubgroup;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -177,7 +178,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||
caps.MaxSampleMaskWords = SaturateToInt(p.limits.maxSampleMaskWords);
|
||||
// Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
|
||||
// same reason: MobileGL's sample-mask state IS a single 32-bit word
|
||||
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
|
||||
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
|
||||
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
|
||||
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
|
||||
// reproduced on this backend. One word is the spec minimum and therefore always legal.
|
||||
// It is also what PipelineCreatePayload::sampleMask is sized for.
|
||||
caps.MaxSampleMaskWords = std::min(SaturateToInt(p.limits.maxSampleMaskWords), 1);
|
||||
caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
||||
@@ -213,6 +222,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
||||
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
|
||||
// One feature covers both stage families here, unlike the ES loader's two extension
|
||||
// tiers; the renderer enables it on the device whenever advertised
|
||||
// (VulkanRenderer::CreateLogicalDeviceAndQueues), so this probe and that enable can
|
||||
// never disagree about the physical device.
|
||||
caps.SupportsTessellationAndGeometryPointSize =
|
||||
supportedFeatures.shaderTessellationAndGeometryPointSize == VK_TRUE;
|
||||
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
|
||||
{
|
||||
// Probe the formats a colour render target actually uses. A driver that refuses the flag
|
||||
@@ -263,7 +278,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
supportsShaderSubgroup ? "true" : "false", caps.SupportsShaderSubgroup ? "true" : "false",
|
||||
subgroupProps.subgroupSize, subgroupProps.supportedStages, subgroupProps.supportedOperations);
|
||||
if (supportsShaderSubgroup && forceDisableShaderSubgroup) {
|
||||
MGLOG_W("Vulkan shader subgroup support forced off by MOBILEGL_DISABLE_SUBGROUP");
|
||||
MGLOG_W("Vulkan shader subgroup support forced off by MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -300,7 +315,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||
caps.MaxSampleMaskWords = SaturateToInt(properties.limits.maxSampleMaskWords);
|
||||
// Clamped to one word, exactly as the GLES loader clamps the driver's value and for the
|
||||
// same reason: MobileGL's sample-mask state IS a single 32-bit word
|
||||
// (RenderState::SampleMaskValue) and SampleMaski_State() raises GL_INVALID_VALUE for any
|
||||
// maskNumber other than 0. dEQP's per-case gluStateReset issues glSampleMaski up to
|
||||
// GL_MAX_SAMPLE_MASK_WORDS, so advertising a device's real 2 would abort the whole glcts
|
||||
// process after every single case - the failure da6f75dbd added the GLES clamp to stop,
|
||||
// reproduced on this backend. One word is the spec minimum and therefore always legal.
|
||||
// It is also what PipelineCreatePayload::sampleMask is sized for.
|
||||
caps.MaxSampleMaskWords = std::min(SaturateToInt(properties.limits.maxSampleMaskWords), 1);
|
||||
caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
||||
@@ -333,6 +356,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||
caps.SupportsWideLines = false;
|
||||
caps.SupportsShaderFloat64 = false;
|
||||
caps.SupportsTessellationAndGeometryPointSize = false;
|
||||
caps.SupportsImageCubeArray = false;
|
||||
caps.Supports2DArrayCompatible3DImages = false;
|
||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||
|
||||
@@ -87,6 +87,13 @@ namespace MobileGL {
|
||||
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
|
||||
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
|
||||
Bool SupportsShaderFloat64 = false;
|
||||
// VkPhysicalDeviceFeatures::shaderTessellationAndGeometryPointSize. Any
|
||||
// tessellation/geometry module declaring OpCapability TessellationPointSize /
|
||||
// GeometryPointSize needs it (VUID-VkShaderModuleCreateInfo-pCode-08740's
|
||||
// capability table); without it the shared phase-B chain demotes the built-in
|
||||
// to an ordinary varying. One feature for both stage families, unlike the ES
|
||||
// loader's two extension tiers.
|
||||
Bool SupportsTessellationAndGeometryPointSize = false;
|
||||
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
|
||||
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
|
||||
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "DriverBugProbes.h"
|
||||
#include "PersistentBufferOrderingProbe.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -126,6 +129,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
GLint packAlignment = 4;
|
||||
GLint packRowLength = 0;
|
||||
// The rest of the pixel-transfer scope. The probes that upload or read back texels
|
||||
// run under whatever scope their caller left - the lazy ones run from live paths,
|
||||
// not just the POST screen - and a caller's skip/row-length/PBO would silently
|
||||
// shear a probe's own data. Saved so a probe can zero them and the caller gets
|
||||
// them back.
|
||||
GLint packSkipPixels = 0;
|
||||
GLint packSkipRows = 0;
|
||||
GLint unpackAlignment = 4;
|
||||
GLint unpackRowLength = 0;
|
||||
GLint unpackImageHeight = 0;
|
||||
GLint unpackSkipPixels = 0;
|
||||
GLint unpackSkipRows = 0;
|
||||
GLint unpackSkipImages = 0;
|
||||
GLint pixelPackBuffer = 0;
|
||||
GLint pixelUnpackBuffer = 0;
|
||||
GLint imageName = 0;
|
||||
GLint imageLevel = 0;
|
||||
GLint imageLayered = 0;
|
||||
@@ -165,6 +183,16 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
|
||||
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
|
||||
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
||||
gl.glGetIntegerv(GL_PACK_SKIP_PIXELS, &state.packSkipPixels);
|
||||
gl.glGetIntegerv(GL_PACK_SKIP_ROWS, &state.packSkipRows);
|
||||
gl.glGetIntegerv(GL_UNPACK_ALIGNMENT, &state.unpackAlignment);
|
||||
gl.glGetIntegerv(GL_UNPACK_ROW_LENGTH, &state.unpackRowLength);
|
||||
gl.glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, &state.unpackImageHeight);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &state.unpackSkipPixels);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_ROWS, &state.unpackSkipRows);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_IMAGES, &state.unpackSkipImages);
|
||||
gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &state.pixelPackBuffer);
|
||||
gl.glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &state.pixelUnpackBuffer);
|
||||
if (gl.glGetFloatv != nullptr) {
|
||||
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor);
|
||||
}
|
||||
@@ -220,6 +248,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (gl.glPixelStorei != nullptr) {
|
||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment);
|
||||
gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, state.packSkipPixels);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_ROWS, state.packSkipRows);
|
||||
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpackAlignment);
|
||||
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpackRowLength);
|
||||
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, state.unpackImageHeight);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, state.unpackSkipPixels);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, state.unpackSkipRows);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, state.unpackSkipImages);
|
||||
}
|
||||
if (gl.glBindBuffer != nullptr) {
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(state.pixelPackBuffer));
|
||||
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(state.pixelUnpackBuffer));
|
||||
}
|
||||
if (gl.glClearColor != nullptr) {
|
||||
gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
|
||||
@@ -1686,6 +1726,519 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== LOCATED INTER-STAGE INTERFACE BLOCKS =====================
|
||||
|
||||
constexpr const char* kIoBlockProbeName = "located interface block";
|
||||
|
||||
// This probe's OWN requirements, not HasEveryEntryPoint's. That one is the geometry
|
||||
// storage probe's list and asks for storage buffers and buffer mapping, which nothing
|
||||
// here touches - borrowing it would let one unresolved SSBO pointer leave a driver that
|
||||
// HAS this defect unrepaired, which is the opposite of what a gate is for. Covers what
|
||||
// BuildProgram, Save/Restore, PrepareForProbeDraw and the draw below actually call.
|
||||
Bool HasIoBlockProbeEntryPoints(const GLESFunctionsTable& gl) {
|
||||
return gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
|
||||
gl.glGetShaderInfoLog && gl.glCreateProgram && gl.glAttachShader &&
|
||||
gl.glLinkProgram && gl.glGetProgramiv && gl.glGetProgramInfoLog &&
|
||||
gl.glDeleteShader && gl.glDeleteProgram && gl.glUseProgram && gl.glGenVertexArrays &&
|
||||
gl.glBindVertexArray && gl.glDeleteVertexArrays && gl.glGenRenderbuffers &&
|
||||
gl.glBindRenderbuffer && gl.glRenderbufferStorage && gl.glDeleteRenderbuffers &&
|
||||
gl.glGenFramebuffers && gl.glBindFramebuffer && gl.glFramebufferRenderbuffer &&
|
||||
gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers && gl.glViewport &&
|
||||
gl.glClearColor && gl.glClear && gl.glDrawArrays && gl.glReadPixels &&
|
||||
gl.glPixelStorei && gl.glGetIntegerv && gl.glGetIntegeri_v && gl.glGetError &&
|
||||
gl.glEnable && gl.glDisable && gl.glIsEnabled;
|
||||
}
|
||||
// Two values that survive an 8-bit target exactly, so the read is a comparison and not
|
||||
// a tolerance: 0.25 -> 64, 0.5 -> 128. A stage that received nothing reads 0/0, which is
|
||||
// nowhere near either.
|
||||
constexpr GLubyte kIoBlockExpectedR = 0x40;
|
||||
constexpr GLubyte kIoBlockExpectedG = 0x80;
|
||||
|
||||
// `@BL@` becomes the layout qualifier under test, or nothing at all for the control.
|
||||
// Position comes from gl_VertexID, so no probe here needs a vertex buffer.
|
||||
String BuildIoBlockVertexSource(const char* blockQualifier) {
|
||||
return format("#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"{}out MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeOut;\n"
|
||||
"void main() {{\n"
|
||||
" vec2 mg_p = vec2((gl_VertexID == 1) ? 3.0 : -1.0,\n"
|
||||
" (gl_VertexID == 2) ? 3.0 : -1.0);\n"
|
||||
" gl_Position = vec4(mg_p, 0.0, 1.0);\n"
|
||||
" mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n"
|
||||
"}}\n",
|
||||
blockQualifier);
|
||||
}
|
||||
|
||||
// The block name changes across the geometry stage, because the two boundaries are two
|
||||
// separate interfaces; one name would also be the in-and-out-under-one-name shape
|
||||
// UniquifyIoBlockNamesPass exists for, and confusing one defect with the other is
|
||||
// exactly what this file's control rule is against.
|
||||
String BuildIoBlockGeometrySource(const char* blockQualifier) {
|
||||
return format("#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"layout(triangles) in;\n"
|
||||
"layout(triangle_strip, max_vertices = 3) out;\n"
|
||||
"{0}in MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeIn[];\n"
|
||||
"{0}out MgProbeBlock2 {{ vec2 mg_probeValue; }} mg_probeOut;\n"
|
||||
"void main() {{\n"
|
||||
" for (int i = 0; i < 3; ++i) {{\n"
|
||||
" gl_Position = gl_in[i].gl_Position;\n"
|
||||
" mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n"
|
||||
" EmitVertex();\n"
|
||||
" }}\n"
|
||||
"}}\n",
|
||||
blockQualifier);
|
||||
}
|
||||
|
||||
String BuildIoBlockFragmentSource(const char* blockQualifier, const char* blockName) {
|
||||
return format("#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"{}in {} {{ vec2 mg_probeValue; }} mg_probeIn;\n"
|
||||
"layout(location = 0) out vec4 mg_probeColor;\n"
|
||||
"void main() {{ mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }}\n",
|
||||
blockQualifier, blockName);
|
||||
}
|
||||
|
||||
// Builds and draws one of the four programs this probe compares and reports whether the
|
||||
// fragment stage received the payload. `outRan` distinguishes "the payload did not
|
||||
// arrive" from "this program could not be built or drawn at all" - the second is
|
||||
// inconclusive and must never become a finding.
|
||||
Bool IoBlockPayloadArrives(const GLESFunctionsTable& gl, const char* blockQualifier,
|
||||
Bool withGeometryStage, Bool& outRan) {
|
||||
outRan = false;
|
||||
Vector<StageSource> stages;
|
||||
stages.push_back({GL_VERTEX_SHADER, BuildIoBlockVertexSource(blockQualifier), "vertex"});
|
||||
if (withGeometryStage) {
|
||||
stages.push_back(
|
||||
{GL_GEOMETRY_SHADER, BuildIoBlockGeometrySource(blockQualifier), "geometry"});
|
||||
}
|
||||
stages.push_back({GL_FRAGMENT_SHADER,
|
||||
BuildIoBlockFragmentSource(blockQualifier,
|
||||
withGeometryStage ? "MgProbeBlock2"
|
||||
: "MgProbeBlock"),
|
||||
"fragment"});
|
||||
|
||||
const ProgramBuild build = BuildProgram(gl, stages, kIoBlockProbeName);
|
||||
if (!build.linked) {
|
||||
if (build.program != 0) gl.glDeleteProgram(build.program);
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
GLuint framebuffer = 0;
|
||||
Bool arrives = false;
|
||||
gl.glGenRenderbuffers(1, &renderbuffer);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
renderbuffer);
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
gl.glUseProgram(build.program);
|
||||
gl.glViewport(0, 0, 1, 1);
|
||||
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
gl.glClear(GL_COLOR_BUFFER_BIT);
|
||||
Drain(gl);
|
||||
gl.glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
if (gl.glGetError() == GL_NO_ERROR) {
|
||||
GLubyte pixel[4] = {0, 0, 0, 0};
|
||||
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||
if (gl.glGetError() == GL_NO_ERROR) {
|
||||
outRan = true;
|
||||
// One bit of slack each way, for a driver that rounds the 8-bit
|
||||
// conversion the other direction.
|
||||
arrives = pixel[0] + 1 >= kIoBlockExpectedR && pixel[0] <= kIoBlockExpectedR + 1 &&
|
||||
pixel[1] + 1 >= kIoBlockExpectedG && pixel[1] <= kIoBlockExpectedG + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
if (renderbuffer != 0) gl.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
gl.glDeleteProgram(build.program);
|
||||
return arrives;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) {
|
||||
LocatedIoBlockMeasurement measurement;
|
||||
if (!HasIoBlockProbeEntryPoints(gl)) return measurement;
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
// The colour mask is not in SavedState - no other probe touches it - so this one saves
|
||||
// and puts back its own. It has to be forced open: a masked channel would read back as
|
||||
// zero and turn a healthy driver into a "payload lost" verdict.
|
||||
GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||
const Bool canMaskColor = gl.glColorMask != nullptr && gl.glGetBooleanv != nullptr;
|
||||
if (canMaskColor) {
|
||||
gl.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
|
||||
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
}
|
||||
GLuint vao = 0;
|
||||
gl.glGenVertexArrays(1, &vao);
|
||||
gl.glBindVertexArray(vao);
|
||||
PrepareForProbeDraw(gl);
|
||||
|
||||
// THE CONTROL, and it runs first: the identical three-stage program with no location on
|
||||
// the blocks. If THAT cannot carry the payload, this driver's problem is not the
|
||||
// qualifier and the probe has no finding to make - reporting one would justify dropping
|
||||
// a qualifier that was never the cause.
|
||||
Bool controlRan = false;
|
||||
const Bool controlArrives = IoBlockPayloadArrives(gl, "", true, controlRan);
|
||||
if (controlRan && controlArrives) {
|
||||
Bool subjectRan = false;
|
||||
const Bool subjectArrives =
|
||||
IoBlockPayloadArrives(gl, "layout(location = 0) ", true, subjectRan);
|
||||
if (subjectRan && !subjectArrives) {
|
||||
measurement.detected = true;
|
||||
// The second control, and the one that scopes the repair: the same located
|
||||
// block between a vertex and a fragment stage. It arrives on the driver this
|
||||
// was characterised on, which is why DirectGLES only drops the qualifier for
|
||||
// programs that have a tessellation or geometry stage. A driver where this one
|
||||
// ALSO fails is losing payloads the repair does not reach, and the report says
|
||||
// so rather than implying the fix is complete.
|
||||
Bool vsFsRan = false;
|
||||
const Bool vsFsArrives =
|
||||
IoBlockPayloadArrives(gl, "layout(location = 0) ", false, vsFsRan);
|
||||
measurement.alsoAffectsVertexToFragment = vsFsRan && !vsFsArrives;
|
||||
}
|
||||
}
|
||||
|
||||
if (vao != 0) {
|
||||
gl.glBindVertexArray(0);
|
||||
gl.glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
if (canMaskColor) {
|
||||
gl.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
|
||||
}
|
||||
Restore(gl, saved);
|
||||
Drain(gl);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) {
|
||||
// One driver per process, and the answer is structural rather than sampled.
|
||||
static const LocatedIoBlockMeasurement measurement = ProbeLocatedIoBlocksLosePayload(gl);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== PACKED16 COPY-IMAGE FIELD ORDER =====================
|
||||
|
||||
constexpr const char* kPacked16CopyProbeName = "packed16 copy-image field order";
|
||||
|
||||
// The shape the KHR-GL4x.copy_image failures pin: a 30x30x12 GL_RGB5_A1 2D array with
|
||||
// the CTS's three-level chain (FUNCTIONAL_TEST_N_LEVELS = 3, makeTextureComplete(0, 2):
|
||||
// 30/15/7 x12; the plain endpoints are 7/3/1), against plain-2D endpoints.
|
||||
//
|
||||
// WHAT THE DEVICE MEASUREMENTS ACTUALLY SHOWED (round 2): the mirrored field order is
|
||||
// a property of the WHOLE ALLOCATION, not of a mip level - a 30x30x12 packed16 array
|
||||
// is born in the mirrored layout at every level, while the small arrays the CTS's
|
||||
// passing iterations used (7- and 15-texel bases; its src/dst dim loop is {7, 15}, so
|
||||
// a base-30 array only ever appears at level 1) are born plain, which is why the
|
||||
// failures looked per-mip-level from the QPA alone. AND the layout is not fixed for
|
||||
// the allocation's lifetime: FBO-ATTACHING the array transitions it to the plain
|
||||
// (renderable) layout, content preserved. That transition is what produced every
|
||||
// seemingly contradictory measurement of this campaign - a probe that direct-read its
|
||||
// array before copying relayouted its own subject and reported the device clean in
|
||||
// the very process whose CTS copies kept mirroring, and the raw matrix's one
|
||||
// "clean" 30x30x12 array was exactly the one that had been direct-read first. It is
|
||||
// also why the CTS's own "source image was not modified" checks always passed: they
|
||||
// read through an FBO attach, after the copy already went wrong. So: subject copies
|
||||
// FIRST, every control that attaches the array AFTER, and because the driver's
|
||||
// allocation heuristic beyond the size threshold is not fully mapped, the probe tries
|
||||
// several allocation recipes of the same client-visible texture and a mirror from ANY
|
||||
// level of ANY recipe is the finding.
|
||||
constexpr GLsizei kPacked16BaseSize = 30;
|
||||
constexpr GLsizei kPacked16Layers = 12;
|
||||
constexpr GLsizei kPacked16DstSize = 7;
|
||||
constexpr GLint kPacked16Levels = 3;
|
||||
|
||||
// One GL_RGB5_A1 texel, as the client word the probe uploads everywhere:
|
||||
// (R, G, B, A) = (0, 1, 3, 1) under GL_UNSIGNED_SHORT_5_5_5_1. Chosen because 5551 is
|
||||
// the one 16-bit packed layout whose field widths are not a palindrome - its mirror
|
||||
// fixes the DIRECTION of the swap - and because this word's mirror differs in every
|
||||
// channel including alpha, so no expansion rounding can confuse the two predictions.
|
||||
constexpr Uint16 kPacked16Word = 0x0047;
|
||||
// What an FBO readback answers for the word, as UNorm8: (0, 1, 3) / 31 and alpha 1.
|
||||
constexpr GLubyte kPacked16Expected[4] = {0, 8, 25, 255};
|
||||
// The same readback when the stored bits are the mirrored re-encoding: 0x0047 decoded
|
||||
// as 5_5_5_1 and re-encoded as 1_5_5_5_REV is 0x8C20, which the destination's non-REV
|
||||
// layout then decodes as (17, 16, 16) / 31 with alpha 0. This is byte-for-byte the
|
||||
// arithmetic behind every failing CTS body (src 0x0047 -> got 0x8C20).
|
||||
constexpr GLubyte kPacked16Mirrored[4] = {140, 132, 132, 0};
|
||||
// A 5-bit step is 255/31 ~ 8.2 UNorm8 codes; half a step accepts every 5-bit-to-8-bit
|
||||
// expansion a driver uses (floor, round, bit replication) while still telling two
|
||||
// adjacent 5-bit values apart.
|
||||
constexpr Int kPacked16Tolerance = 4;
|
||||
|
||||
// The allocation recipes the probe tries. Same client-visible texture, same data -
|
||||
// only the order and the filter state during the uploads move, because those are the
|
||||
// knobs the driver's layout heuristic was measured keying on (differently in
|
||||
// different contexts).
|
||||
enum class Packed16Recipe : Uint8 {
|
||||
// glTexImage3D per level on a fresh texture at driver defaults, parameters after:
|
||||
// the order a freshly minted MobileGL backend texture performs (the storage sync
|
||||
// runs before the parameter re-push, see SyncTextureObjectToBackend).
|
||||
UploadsFirst,
|
||||
// NEAREST and MAX_LEVEL set before the uploads: the shape an application that
|
||||
// configures its sampler state ahead of its data gets.
|
||||
ParamsFirst,
|
||||
// MAX_LEVEL bounded but MIN_FILTER left at its mipmapped default: the CTS
|
||||
// copy-test texture verbatim - copy tests never touch filters, and the chain is
|
||||
// complete because all three levels exist under MAX_LEVEL = 2.
|
||||
CtsShape,
|
||||
};
|
||||
constexpr Packed16Recipe kPacked16Recipes[] = {Packed16Recipe::UploadsFirst,
|
||||
Packed16Recipe::ParamsFirst,
|
||||
Packed16Recipe::CtsShape};
|
||||
const char* Packed16RecipeName(Packed16Recipe recipe) {
|
||||
switch (recipe) {
|
||||
case Packed16Recipe::UploadsFirst: return "uploads-first";
|
||||
case Packed16Recipe::ParamsFirst: return "params-first";
|
||||
case Packed16Recipe::CtsShape: return "cts-shape";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
// A three-level GL_RGB5_A1 2D array (30/15/7, twelve layers each, every texel holding
|
||||
// kPacked16Word) allocated per `recipe`. Every recipe ends mipmap-complete - some
|
||||
// drivers refuse glCopyImageSubData on an incomplete texture.
|
||||
GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl, Packed16Recipe recipe) {
|
||||
GLuint texture = 0;
|
||||
gl.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
if (recipe == Packed16Recipe::ParamsFirst) {
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
}
|
||||
if (recipe != Packed16Recipe::UploadsFirst) {
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
|
||||
}
|
||||
for (GLint level = 0; level < kPacked16Levels; ++level) {
|
||||
const GLsizei size = kPacked16BaseSize >> level;
|
||||
const Vector<Uint16> words(
|
||||
static_cast<SizeT>(size) * static_cast<SizeT>(size) * kPacked16Layers, kPacked16Word);
|
||||
gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGB5_A1, size, size, kPacked16Layers, 0,
|
||||
GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, words.data());
|
||||
}
|
||||
if (recipe == Packed16Recipe::UploadsFirst) {
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
|
||||
}
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// The plain-2D destination, three levels (7/3/1) like the CTS's, every level filled
|
||||
// with 0xFFFF - the CTS's own (1,1,1,1) destination fill - so a copy that silently
|
||||
// did nothing reads as "no verdict" rather than as either prediction.
|
||||
// A plain-2D endpoint with the CTS's three-level 7/3/1 chain, every texel of every
|
||||
// level holding `fill`: 0xFFFF (the CTS's own (1,1,1,1) destination fill, so a copy
|
||||
// that silently did nothing reads as "no verdict" rather than as either prediction),
|
||||
// or kPacked16Word for the machinery control's source. Uploads first, parameters
|
||||
// after, for the same in-situ fidelity as the array above - this is the allocation
|
||||
// discipline every MobileGL-minted texture gets, and the shape the failing bodies'
|
||||
// clean plain endpoints had.
|
||||
GLuint MakePacked16FlatTexture(const GLESFunctionsTable& gl, Uint16 fill) {
|
||||
GLuint texture = 0;
|
||||
gl.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
gl.glBindTexture(GL_TEXTURE_2D, texture);
|
||||
for (GLint level = 0; level < kPacked16Levels; ++level) {
|
||||
const GLsizei size = std::max<GLsizei>(kPacked16DstSize >> level, 1);
|
||||
const Vector<Uint16> texels(static_cast<SizeT>(size) * size, fill);
|
||||
gl.glTexImage2D(GL_TEXTURE_2D, level, GL_RGB5_A1, size, size, 0, GL_RGBA,
|
||||
GL_UNSIGNED_SHORT_5_5_5_1, texels.data());
|
||||
}
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
|
||||
gl.glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Texel (0, 0) of a 2D level 0, or of layer 0 of an array's `level`, through a
|
||||
// framebuffer of its own. False when the attachment is incomplete or the read errors -
|
||||
// both are declines, not verdicts.
|
||||
Bool ReadPacked16Texel(const GLESFunctionsTable& gl, GLuint texture, Bool isArray, GLint level,
|
||||
GLubyte out[4]) {
|
||||
GLuint framebuffer = 0;
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
if (framebuffer == 0) return false;
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
if (isArray) {
|
||||
gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, 0);
|
||||
} else {
|
||||
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
|
||||
}
|
||||
Bool read = false;
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
Drain(gl);
|
||||
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||
read = gl.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
Drain(gl);
|
||||
return read;
|
||||
}
|
||||
|
||||
// Copies a kPacked16DstSize-square region out of (source, sourceTarget, sourceLevel)
|
||||
// layer 0 onto a freshly 0xFFFF-filled 2D destination and hands back the destination's
|
||||
// texel (0, 0). False when the copy raised an error or the readback could not run.
|
||||
Bool Packed16CopyLandsTexel(const GLESFunctionsTable& gl, GLuint source, GLenum sourceTarget,
|
||||
GLint sourceLevel, GLubyte out[4]) {
|
||||
const GLuint destination = MakePacked16FlatTexture(gl, Uint16{0xFFFF});
|
||||
if (destination == 0) return false;
|
||||
Drain(gl);
|
||||
gl.glCopyImageSubData(source, sourceTarget, sourceLevel, 0, 0, 0, destination,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, kPacked16DstSize, kPacked16DstSize, 1);
|
||||
const Bool copied = gl.glGetError() == GL_NO_ERROR;
|
||||
const Bool read = copied && ReadPacked16Texel(gl, destination, false, 0, out);
|
||||
gl.glDeleteTextures(1, &destination);
|
||||
Drain(gl);
|
||||
return read;
|
||||
}
|
||||
|
||||
Bool Packed16TexelNear(const GLubyte got[4], const GLubyte want[4]) {
|
||||
for (Int i = 0; i < 4; ++i) {
|
||||
const Int delta = static_cast<Int>(got[i]) - static_cast<Int>(want[i]);
|
||||
if (delta > kPacked16Tolerance || delta < -kPacked16Tolerance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// One recipe's whole measurement: allocate, both subject copies, THEN the round-trip
|
||||
// control. The order is load-bearing: FBO-ATTACHING THE ARRAY TRANSITIONS IT to the
|
||||
// plain (renderable) layout on the affected driver, so a round-trip read taken before
|
||||
// the copies RELAYOUTS the subject and measures a texture the application's copy
|
||||
// never sees - round two's first deployment did exactly that and reported the device
|
||||
// clean while the CTS bodies kept failing in the same process. Copies first, the
|
||||
// control after: the attach-driven transition preserves content, so the read still
|
||||
// answers "the upload was intact" without disturbing what the copies measured. Only a
|
||||
// mirror that matches the PREDICTION while that control holds counts; everything else
|
||||
// is that recipe's no-verdict (logged as such).
|
||||
Bool RunPacked16Recipe(const GLESFunctionsTable& gl, Packed16Recipe recipe) {
|
||||
Bool mirrored = false;
|
||||
const GLuint array = MakePacked16ArrayTexture(gl, recipe);
|
||||
GLubyte direct[4] = {0, 0, 0, 0};
|
||||
GLubyte level0[4] = {0, 0, 0, 0};
|
||||
GLubyte level1[4] = {0, 0, 0, 0};
|
||||
if (array == 0 || !Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 0, level0) ||
|
||||
!Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 1, level1)) {
|
||||
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (a subject copy could not run)",
|
||||
kPacked16CopyProbeName, Packed16RecipeName(recipe));
|
||||
} else if (!ReadPacked16Texel(gl, array, true, 1, direct) ||
|
||||
!Packed16TexelNear(direct, kPacked16Expected)) {
|
||||
// The recipe's own round trip: reading the level directly decodes the driver's
|
||||
// own storage and must deliver the word whatever layout it picked. A wrong
|
||||
// answer means the UPLOAD is what corrupts - a different defect, and one the
|
||||
// widening's raw-copy reasoning says nothing about.
|
||||
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (the array's own level-1 readback "
|
||||
"answered (%d, %d, %d, %d) instead of the word - the upload, not the "
|
||||
"copy, is what diverges)",
|
||||
kPacked16CopyProbeName, Packed16RecipeName(recipe), direct[0], direct[1],
|
||||
direct[2], direct[3]);
|
||||
} else if (Packed16TexelNear(level0, kPacked16Mirrored) ||
|
||||
Packed16TexelNear(level1, kPacked16Mirrored)) {
|
||||
mirrored = true;
|
||||
MGLOG_I("[driver-bug] %s probe [%s]: copies delivered level 0 (%d, %d, %d, %d) / "
|
||||
"level 1 (%d, %d, %d, %d) - the 1_5_5_5_REV re-encoding of the word - "
|
||||
"THIS ALLOCATION'S FIELD ORDER IS MIRRORED",
|
||||
kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1],
|
||||
level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]);
|
||||
} else if (Packed16TexelNear(level0, kPacked16Expected) &&
|
||||
Packed16TexelNear(level1, kPacked16Expected)) {
|
||||
MGLOG_I("[driver-bug] %s probe [%s]: both levels copied the word intact",
|
||||
kPacked16CopyProbeName, Packed16RecipeName(recipe));
|
||||
} else {
|
||||
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (copies read back level 0 "
|
||||
"(%d, %d, %d, %d) / level 1 (%d, %d, %d, %d), neither the word nor its "
|
||||
"mirror)",
|
||||
kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1],
|
||||
level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]);
|
||||
}
|
||||
if (array != 0) gl.glDeleteTextures(1, &array);
|
||||
return mirrored;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
|
||||
if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexParameteri || !gl.glTexImage2D ||
|
||||
!gl.glTexImage3D || !gl.glDeleteTextures || !gl.glCopyImageSubData || !gl.glGenFramebuffers ||
|
||||
!gl.glBindFramebuffer || !gl.glFramebufferTexture2D || !gl.glFramebufferTextureLayer ||
|
||||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glReadBuffer ||
|
||||
!gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
// The uploads and readbacks below run under the probe's own tight pixel-transfer
|
||||
// scope - a caller's skip/row-length/PBO would shear the probe's data into a false
|
||||
// verdict either way. Restore puts the caller's scope back with the rest.
|
||||
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
|
||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
gl.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
if (gl.glBindBuffer != nullptr) {
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
}
|
||||
Drain(gl);
|
||||
|
||||
Bool detected = false;
|
||||
const GLuint flatSource = MakePacked16FlatTexture(gl, kPacked16Word);
|
||||
GLubyte machinery[4] = {0, 0, 0, 0};
|
||||
// THE MACHINERY CONTROL: a copy between two 2D images of the same three-level shape
|
||||
// and allocation discipline. Two identical allocations share the driver's layout
|
||||
// whatever it is, so this must deliver the word on ANY driver that can run copy_image
|
||||
// on these formats at all - a driver that cannot reaches no verdict instead of being
|
||||
// reported as this.
|
||||
if (flatSource == 0 || !Packed16CopyLandsTexel(gl, flatSource, GL_TEXTURE_2D, 0, machinery)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control "
|
||||
"could not run)",
|
||||
kPacked16CopyProbeName);
|
||||
} else if (!Packed16TexelNear(machinery, kPacked16Expected)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control "
|
||||
"read back (%d, %d, %d, %d) instead of the word's (%d, %d, %d, %d))",
|
||||
kPacked16CopyProbeName, machinery[0], machinery[1], machinery[2], machinery[3],
|
||||
kPacked16Expected[0], kPacked16Expected[1], kPacked16Expected[2], kPacked16Expected[3]);
|
||||
} else {
|
||||
// THE SUBJECTS: every allocation recipe of the same array, each with its own
|
||||
// round-trip control; a mirror from any level of any recipe is the finding. Every
|
||||
// recipe logs its own verdict either way, so a device run always shows whether
|
||||
// this probe executed and what each allocation delivered - a silent clean path
|
||||
// would be indistinguishable from a probe that never ran.
|
||||
for (const Packed16Recipe recipe : kPacked16Recipes) {
|
||||
detected = RunPacked16Recipe(gl, recipe) || detected;
|
||||
}
|
||||
}
|
||||
if (flatSource != 0) gl.glDeleteTextures(1, &flatSource);
|
||||
Restore(gl, saved);
|
||||
return detected;
|
||||
}
|
||||
|
||||
Bool CopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
|
||||
// One driver per process, and the answer is structural (the driver's storage layout
|
||||
// for a shape), not sampled.
|
||||
static const Bool mirrored = ProbeCopyImageMirrorsPacked16FieldOrder(gl);
|
||||
return mirrored;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
|
||||
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
|
||||
@@ -1808,6 +2361,95 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
percentOf(measurement.emittedShapeMismatchedTexels))};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeLocatedIoBlockPayloadBug(const GLESFunctionsTable& gl) {
|
||||
const LocatedIoBlockMeasurement& measurement = LocatedIoBlocksLosePayload(gl);
|
||||
if (!measurement.detected) return std::nullopt;
|
||||
String detail =
|
||||
"an inter-stage interface block that carries an explicit layout(location = N) "
|
||||
"delivers NOTHING once a geometry (or tessellation) stage is in the pipeline: the "
|
||||
"stages compile, the program links with an empty info log, the draw raises no "
|
||||
"error, and the consuming stage reads zeroes. The byte-identical program with the "
|
||||
"qualifier removed from the blocks carries its payload correctly, which is what "
|
||||
"makes this a LOCATION defect rather than an interface-block one - blocks "
|
||||
"themselves work here";
|
||||
detail += measurement.alsoAffectsVertexToFragment
|
||||
? ". A located block between a VERTEX and a FRAGMENT stage is lost on "
|
||||
"this driver too, so the defect is wider than the repair below "
|
||||
"reaches: MobileGL only drops the qualifier for programs that have a "
|
||||
"tessellation or geometry stage, and a located block in a plain "
|
||||
"vertex+fragment program is still emitted as the application wrote it"
|
||||
: ". A located block between a VERTEX and a FRAGMENT stage is delivered "
|
||||
"correctly on the same driver, which is what scopes the repair";
|
||||
// The repair can be switched off from the environment, and a report that said
|
||||
// "Fixed" while the strip was disabled would be describing a build nobody is
|
||||
// running. The verdict follows what this process will actually do, not what the
|
||||
// code is capable of.
|
||||
const Bool repairDisabled =
|
||||
MG_Config::Features.EsprytUnlocatedIoBlocks == MG_Config::QuirkOverride::ForceOff;
|
||||
if (repairDisabled) {
|
||||
detail +=
|
||||
". THE REPAIR IS DISABLED in this process: MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS "
|
||||
"is set to force located blocks ON, so DirectGLES emits the location "
|
||||
"qualifier the driver cannot honour and the payload is lost. Unset the "
|
||||
"variable to get the repair back";
|
||||
} else {
|
||||
detail +=
|
||||
". MobileGL emits a tessellation/geometry program's interface blocks with no "
|
||||
"location qualifier at all (StripIoBlockLocationsPass) and lets ES match them "
|
||||
"by block name and member sequence, which it does; the locations were invented "
|
||||
"by the cross-stage IO resolver rather than written by the application";
|
||||
}
|
||||
return DriverBugFinding{"Located inter-stage interface blocks carry no payload",
|
||||
(repairDisabled || measurement.alsoAffectsVertexToFragment)
|
||||
? DriverBugVerdict::Unfixable
|
||||
: DriverBugVerdict::Fixed,
|
||||
Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeCopyImagePacked16FieldOrderBug(const GLESFunctionsTable& gl) {
|
||||
if (!CopyImageMirrorsPacked16FieldOrder(gl)) return std::nullopt;
|
||||
// The mitigation is a knob (MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE), so the row consults
|
||||
// it: under ForceOff - the documented negative control - the corruption is
|
||||
// replayed verbatim, and a hardcoded "Fixed" would be exactly the kind of
|
||||
// reassurance this file exists to refuse. Auto and ForceOn both widen once this
|
||||
// probe has fired. Should POST ever run before env parsing, the field still holds
|
||||
// its Auto default - which is also what the widening itself consults, so the row
|
||||
// and the behaviour cannot disagree.
|
||||
const Bool widened = MG_Config::Features.EsprytWidenPacked16Storage !=
|
||||
MG_Config::QuirkOverride::ForceOff;
|
||||
String detail =
|
||||
"the driver stores SOME 16-bit packed images (RGB565 / RGB5_A1 / RGBA4) with the "
|
||||
"R/G/B/A fields packed from the other end of the word - which allocations get the "
|
||||
"*_REV layout depends on shape and context history (measured here on a 30x30x12 "
|
||||
"three-level 2D array, every level of it) - so a glCopyImageSubData, a raw "
|
||||
"texel-block move, between a mirrored allocation and a plain one lands the fields "
|
||||
"reversed (a 5551 word 0x0047 arrives as 0x8C20). Uploads and readbacks of the "
|
||||
"same image are clean - the driver decodes its own layout consistently, which is "
|
||||
"this probe's second control - so only the raw-copy path ever crosses the two "
|
||||
"layouts. ";
|
||||
if (widened) {
|
||||
detail +=
|
||||
"MobileGL stores these three formats as 8-bit-per-channel ES storage on this "
|
||||
"driver instead (GL_RGB8 / GL_RGBA8, the storage their canonical shadow "
|
||||
"already holds and the client word round-trips through exactly), so no "
|
||||
"16-bit packed image is left for a copy to disagree about, at twice the "
|
||||
"memory for images of those formats; override with "
|
||||
"MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE";
|
||||
return DriverBugFinding{
|
||||
"glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images",
|
||||
DriverBugVerdict::Fixed, detail};
|
||||
}
|
||||
detail += "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=0 keeps the native narrow storage, so such "
|
||||
"copies are left exactly as the driver delivers them, mirrored words included";
|
||||
return DriverBugFinding{
|
||||
"glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images",
|
||||
DriverBugVerdict::Unfixable, detail};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbePersistentBufferOrderingBug(const GLESFunctionsTable& gl) {
|
||||
return DescribePersistentBufferOrderingBug(ProbePersistentBufferUpdateOrdering(gl));
|
||||
}
|
||||
|
||||
// The table. One row per known driver bug; see the header for how to add a sibling.
|
||||
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
||||
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
||||
@@ -1818,6 +2460,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
&ProbeImageCoherencyResidualBug,
|
||||
&ProbeExplicitVertexInputLocationCeilingBug,
|
||||
&ProbeLayeredBlitDestinationBug,
|
||||
&ProbeLocatedIoBlockPayloadBug,
|
||||
&ProbeCopyImagePacked16FieldOrderBug,
|
||||
&ProbePersistentBufferOrderingBug,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -55,6 +55,44 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
String detail;
|
||||
};
|
||||
|
||||
// What the located-interface-block probe measured.
|
||||
struct LocatedIoBlockMeasurement {
|
||||
// The driver delivers nothing through an inter-stage interface block that carries an
|
||||
// explicit layout(location=) once a geometry stage is in the pipeline. The only field
|
||||
// any caller's behaviour depends on.
|
||||
Bool detected = false;
|
||||
// ...and it does the same WITHOUT a geometry stage, i.e. between a vertex and a
|
||||
// fragment stage. False on the device this was characterised on, and reported because
|
||||
// DirectGLES's repair is scoped to tessellation/geometry programs: a driver that
|
||||
// answered true here would be losing block payloads the repair does not reach.
|
||||
Bool alsoAffectsVertexToFragment = false;
|
||||
};
|
||||
|
||||
// Draws one full-viewport triangle through VS+GS+FS whose two interface blocks carry an
|
||||
// explicit layout(location = 0), and reports whether the payload the vertex stage wrote
|
||||
// reached the fragment stage.
|
||||
//
|
||||
// The Mali-G1-Ultra ES driver (r54p1) delivers ZEROES: the stages compile, the program
|
||||
// links with an empty info log, the draw runs without error, and the block is empty. It is
|
||||
// the whole of the KHR-GLxx.shading_language_420pack interface-block group's failures on
|
||||
// that device, and of a further 21 tessellation and geometry bodies beside it.
|
||||
//
|
||||
// TWO CONTROLS, and the first is why this is a LOCATION finding rather than a block one:
|
||||
// (1) the identical three-stage program with the qualifier removed from both blocks must
|
||||
// deliver its payload - without that, "this driver cannot carry an interface block through
|
||||
// a geometry stage" would be the claim, which is false and would justify flattening every
|
||||
// block on the device; and (2) a two-stage vertex-to-fragment program with a LOCATED block
|
||||
// is measured separately, because that one works on the affected driver and is what scopes
|
||||
// the repair to programs with a tessellation or geometry stage.
|
||||
//
|
||||
// Returns `detected` false when an entry point is missing, when the driver has no geometry
|
||||
// stage, or when the unlocated control fails - an inconclusive probe must never be reported
|
||||
// as a bug, and must never arm the repair. Restores every piece of GL state it touches.
|
||||
LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeLocatedIoBlocksLosePayload(), evaluated at most once per process.
|
||||
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the
|
||||
// copy landed where it was asked to. Returns true only when the destination layer is ignored
|
||||
// while the control lands correctly.
|
||||
@@ -261,6 +299,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Copies one known GL_UNSIGNED_SHORT_5_5_5_1 word out of both mip levels of a GL_RGB5_A1
|
||||
// 2D array into plain 2D images with glCopyImageSubData and reads the landed texels back
|
||||
// - for SEVERAL ALLOCATION RECIPES of the same array. Returns true only when a copy from
|
||||
// any level of any recipe delivers the word's 5_5_5_1 <-> 1_5_5_5_REV field-order mirror
|
||||
// while the controls below hold.
|
||||
//
|
||||
// The affected Mali stores SOME 16-bit packed allocations (RGB565 / RGB5_A1 / RGBA4) with
|
||||
// their fields packed from the other end of the word. The mirrored layout is an
|
||||
// ALLOCATION property, not a mip-level one - the failing device's 30x30x12 array is born
|
||||
// mirrored at level 0 and level 1 alike, which is what vetoed the first deployment's
|
||||
// "level 0 is the clean control" design - and it is not fixed for the allocation's
|
||||
// lifetime either: FBO-ATTACHING the array transitions it to the plain layout, content
|
||||
// preserved, which is why a probe that direct-reads its array before copying relayouts
|
||||
// its own subject and measures a texture the application's copies never see (the second
|
||||
// deployment's miss), and why the CTS's "source not modified" checks always passed. The
|
||||
// probe builds the CTS's failing shape (three-level chains both endpoints: 30/15/7 x12
|
||||
// array, 7/3/1 plain, FUNCTIONAL_TEST_N_LEVELS = 3) with several allocation recipes,
|
||||
// copies FIRST, in situ, and a mirror delivered from any level of any recipe is the
|
||||
// finding. Uploads and readbacks decode each image's layout of the moment consistently,
|
||||
// so nothing but a raw texel-block move can see the divergence - which is exactly what
|
||||
// glCopyImageSubData is defined to be, and why the whole KHR-GL4x.copy_image
|
||||
// rgb5/rgb5_a1/rgba4 x *2d_array* matrix fails there while every other suite touching
|
||||
// these formats passes.
|
||||
//
|
||||
// CONTROLS. The machinery: an identical copy between two SAME-shape plain-2D images,
|
||||
// which share a layout whatever it is, so it must deliver the word on any driver that can
|
||||
// run copy_image on these formats - a driver that cannot reaches no verdict instead of
|
||||
// being reported as this. And per recipe, AFTER its subject copies, the array's own round
|
||||
// trip: a direct FBO readback of its level 1 must answer the word, or the UPLOAD is what
|
||||
// corrupts - a different defect. The subjects must also match the mirror PREDICTION, not
|
||||
// merely differ from the word - a copy that delivered anything else is a different defect
|
||||
// and reaches no verdict either. Restores every piece of GL state it touches.
|
||||
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeCopyImageMirrorsPacked16FieldOrder(), evaluated at most once per process. The
|
||||
// DirectGLES format normalization consults this to decide whether the three 16-bit packed
|
||||
// normalized formats must be stored as 8-bit-per-channel ES storage (see
|
||||
// PixelFormatNormalizeOptionBit::WidenPacked16Norm).
|
||||
Bool CopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Every known driver bug this GLES driver actually has. Bugs it does not have are absent,
|
||||
// so an unaffected device renders an empty section rather than a wall of "not affected".
|
||||
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "DriverPost.h"
|
||||
#include "DriverPostIterationRPWitness.h"
|
||||
#include "DriverPostIterationRPWitnessSpv.h"
|
||||
#include "PrimitivesGeneratedNoXfbProbe.h"
|
||||
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
@@ -1555,9 +1556,380 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
disabledNote);
|
||||
}
|
||||
|
||||
// GL_PRIMITIVES_GENERATED for draws made with transform feedback INACTIVE. GL
|
||||
// defines the query to count them; the DirectVulkan backend serves it from the
|
||||
// stream query's primitivesNeeded, and an affected Mali driver answers 0 there
|
||||
// unless a capture span is open - the exact shape the CTS uses to measure the
|
||||
// tessellator (see PrimitivesGeneratedNoXfbProbe.h). One row:
|
||||
// PASS - the stream query counts the capture-less draw exactly.
|
||||
// WARN - it answers 0, and the CLIPPING_INPUT_PRIMITIVES statistics control
|
||||
// on the same draw answers exactly right, so the renderer substitutes
|
||||
// a pipeline-statistics pool for such draws (the same probe, run at
|
||||
// renderer bring-up, is what arms it).
|
||||
// FAIL (optional) - it answers 0 with no working substitute, or the probe
|
||||
// could not reach a verdict; applications sizing capture buffers from
|
||||
// the query get 0.
|
||||
// Throwaway device on purpose, like every probe here: the row reports the
|
||||
// driver, not the renderer's configuration - MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE
|
||||
// steers the renderer, never this row.
|
||||
void ProbeVulkanPrimitivesGeneratedNoXfb(ReportBuilder& builder,
|
||||
PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 graphicsQueueFamilyIndex,
|
||||
const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
const VkPhysicalDeviceFeatures& features,
|
||||
PFN_vkGetPhysicalDeviceFeatures2 getFeatures2,
|
||||
PFN_vkGetPhysicalDeviceProperties2 getProperties2) {
|
||||
constexpr const char* RowName = "Primitives-generated query without capture";
|
||||
const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); };
|
||||
|
||||
if (!HasVkExtension(deviceExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME) ||
|
||||
getFeatures2 == nullptr || getProperties2 == nullptr) {
|
||||
fail("VK_EXT_transform_feedback is unavailable, so the backend has no GPU counter for "
|
||||
"GL_PRIMITIVES_GENERATED at all - with or without a capture");
|
||||
return;
|
||||
}
|
||||
VkPhysicalDeviceTransformFeedbackFeaturesEXT xfbFeatures{};
|
||||
xfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT;
|
||||
VkPhysicalDeviceFeatures2 features2{};
|
||||
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
features2.pNext = &xfbFeatures;
|
||||
getFeatures2(physicalDevice, &features2);
|
||||
VkPhysicalDeviceTransformFeedbackPropertiesEXT xfbProperties{};
|
||||
xfbProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT;
|
||||
VkPhysicalDeviceProperties2 properties2{};
|
||||
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
properties2.pNext = &xfbProperties;
|
||||
getProperties2(physicalDevice, &properties2);
|
||||
if (xfbFeatures.transformFeedback != VK_TRUE || xfbProperties.transformFeedbackQueries != VK_TRUE) {
|
||||
fail("the device has VK_EXT_transform_feedback but no usable stream queries "
|
||||
"(transformFeedbackQueries = false); GL_PRIMITIVES_GENERATED and "
|
||||
"GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN queries answer 0");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto vkCreateDeviceFn =
|
||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||
const auto vkDestroyDeviceFn =
|
||||
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
|
||||
const auto vkGetDeviceQueueFn =
|
||||
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
|
||||
if (vkCreateDeviceFn == nullptr || vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr) {
|
||||
fail("vkGetInstanceProcAddr could not resolve the device-creation entry points");
|
||||
return;
|
||||
}
|
||||
|
||||
const Float queuePriority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queueInfo{};
|
||||
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
||||
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
queueInfo.queueCount = 1;
|
||||
queueInfo.pQueuePriorities = &queuePriority;
|
||||
|
||||
// Only what the probe itself needs: the transform feedback feature (a
|
||||
// stream-query pool may not be created without it), the two candidate
|
||||
// substitutes' features, and tessellationShader for the PATCHES shape -
|
||||
// each only where the device has it. The dedicated
|
||||
// primitives-generated query is taken with BOTH its bits or not at all,
|
||||
// mirroring the renderer (without the discard bit two of the three
|
||||
// shapes may not run inside it).
|
||||
VkPhysicalDeviceFeatures enabledFeatures{};
|
||||
enabledFeatures.pipelineStatisticsQuery = features.pipelineStatisticsQuery;
|
||||
enabledFeatures.tessellationShader = features.tessellationShader;
|
||||
VkPhysicalDeviceTransformFeedbackFeaturesEXT enabledXfbFeatures{};
|
||||
enabledXfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT;
|
||||
enabledXfbFeatures.transformFeedback = VK_TRUE;
|
||||
const char* enabledExtensions[2] = {VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME, nullptr};
|
||||
Uint32 enabledExtensionCount = 1;
|
||||
|
||||
Bool primitivesGeneratedQueryUsable = false;
|
||||
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT enabledPgqFeatures{};
|
||||
enabledPgqFeatures.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||
if (HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) {
|
||||
VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT pgqQuery{};
|
||||
pgqQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT;
|
||||
VkPhysicalDeviceFeatures2 pgqFeatures2{};
|
||||
pgqFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
pgqFeatures2.pNext = &pgqQuery;
|
||||
getFeatures2(physicalDevice, &pgqFeatures2);
|
||||
if (pgqQuery.primitivesGeneratedQuery == VK_TRUE &&
|
||||
pgqQuery.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) {
|
||||
primitivesGeneratedQueryUsable = true;
|
||||
enabledPgqFeatures.primitivesGeneratedQuery = VK_TRUE;
|
||||
enabledPgqFeatures.primitivesGeneratedQueryWithRasterizerDiscard = VK_TRUE;
|
||||
enabledPgqFeatures.pNext = &enabledXfbFeatures;
|
||||
enabledExtensions[enabledExtensionCount++] =
|
||||
VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
VkDeviceCreateInfo deviceInfo{};
|
||||
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||
deviceInfo.pNext = primitivesGeneratedQueryUsable
|
||||
? static_cast<const void*>(&enabledPgqFeatures)
|
||||
: static_cast<const void*>(&enabledXfbFeatures);
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||
deviceInfo.enabledExtensionCount = enabledExtensionCount;
|
||||
deviceInfo.ppEnabledExtensionNames = enabledExtensions;
|
||||
deviceInfo.pEnabledFeatures = &enabledFeatures;
|
||||
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
const VkResult createResult = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
|
||||
if (createResult != VK_SUCCESS || device == VK_NULL_HANDLE) {
|
||||
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(createResult)));
|
||||
return;
|
||||
}
|
||||
// The probe's own teardown destroys (and idle-waits) everything it created -
|
||||
// EXCEPT when its bounded fence wait expires, where it deliberately leaks
|
||||
// every child object rather than touch a possibly hung GPU. This device must
|
||||
// then leak with them: vkDestroyDevice requires its children destroyed and its
|
||||
// queues idle, and on the driver that just missed a 5 s deadline the realistic
|
||||
// outcome is a block inside vkDestroyDevice - the POST hang the bound exists to
|
||||
// prevent. Same shape as the timestamp probe's guard above and the iterationRP
|
||||
// witness's below.
|
||||
Bool probeFenceWaitTimedOut = false;
|
||||
const ScopeGuard destroyDevice([&]() {
|
||||
if (probeFenceWaitTimedOut) {
|
||||
return;
|
||||
}
|
||||
vkDestroyDeviceFn(device, nullptr);
|
||||
});
|
||||
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue);
|
||||
if (queue == VK_NULL_HANDLE) {
|
||||
fail("vkGetDeviceQueue returned a null graphics queue");
|
||||
return;
|
||||
}
|
||||
|
||||
PrimitivesGeneratedNoXfbProbeContext probeContext;
|
||||
probeContext.device = device;
|
||||
probeContext.queue = queue;
|
||||
probeContext.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
probeContext.transformFeedbackQueriesUsable = true;
|
||||
probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable;
|
||||
probeContext.pipelineStatisticsEnabled = enabledFeatures.pipelineStatisticsQuery == VK_TRUE;
|
||||
probeContext.tessellationEnabled = enabledFeatures.tessellationShader == VK_TRUE;
|
||||
auto& fns = probeContext.fns;
|
||||
const auto resolve = [&](const char* name) { return getInstanceProcAddr(instance, name); };
|
||||
fns.vkCreateCommandPool = reinterpret_cast<PFN_vkCreateCommandPool>(resolve("vkCreateCommandPool"));
|
||||
fns.vkDestroyCommandPool =
|
||||
reinterpret_cast<PFN_vkDestroyCommandPool>(resolve("vkDestroyCommandPool"));
|
||||
fns.vkAllocateCommandBuffers =
|
||||
reinterpret_cast<PFN_vkAllocateCommandBuffers>(resolve("vkAllocateCommandBuffers"));
|
||||
fns.vkBeginCommandBuffer =
|
||||
reinterpret_cast<PFN_vkBeginCommandBuffer>(resolve("vkBeginCommandBuffer"));
|
||||
fns.vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(resolve("vkEndCommandBuffer"));
|
||||
fns.vkCreateQueryPool = reinterpret_cast<PFN_vkCreateQueryPool>(resolve("vkCreateQueryPool"));
|
||||
fns.vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(resolve("vkDestroyQueryPool"));
|
||||
fns.vkCmdResetQueryPool = reinterpret_cast<PFN_vkCmdResetQueryPool>(resolve("vkCmdResetQueryPool"));
|
||||
fns.vkCmdBeginQuery = reinterpret_cast<PFN_vkCmdBeginQuery>(resolve("vkCmdBeginQuery"));
|
||||
fns.vkCmdEndQuery = reinterpret_cast<PFN_vkCmdEndQuery>(resolve("vkCmdEndQuery"));
|
||||
fns.vkCmdBeginQueryIndexedEXT =
|
||||
reinterpret_cast<PFN_vkCmdBeginQueryIndexedEXT>(resolve("vkCmdBeginQueryIndexedEXT"));
|
||||
fns.vkCmdEndQueryIndexedEXT =
|
||||
reinterpret_cast<PFN_vkCmdEndQueryIndexedEXT>(resolve("vkCmdEndQueryIndexedEXT"));
|
||||
fns.vkCreateRenderPass = reinterpret_cast<PFN_vkCreateRenderPass>(resolve("vkCreateRenderPass"));
|
||||
fns.vkDestroyRenderPass =
|
||||
reinterpret_cast<PFN_vkDestroyRenderPass>(resolve("vkDestroyRenderPass"));
|
||||
fns.vkCreateFramebuffer =
|
||||
reinterpret_cast<PFN_vkCreateFramebuffer>(resolve("vkCreateFramebuffer"));
|
||||
fns.vkDestroyFramebuffer =
|
||||
reinterpret_cast<PFN_vkDestroyFramebuffer>(resolve("vkDestroyFramebuffer"));
|
||||
fns.vkCmdBeginRenderPass =
|
||||
reinterpret_cast<PFN_vkCmdBeginRenderPass>(resolve("vkCmdBeginRenderPass"));
|
||||
fns.vkCmdEndRenderPass = reinterpret_cast<PFN_vkCmdEndRenderPass>(resolve("vkCmdEndRenderPass"));
|
||||
fns.vkCreateShaderModule =
|
||||
reinterpret_cast<PFN_vkCreateShaderModule>(resolve("vkCreateShaderModule"));
|
||||
fns.vkDestroyShaderModule =
|
||||
reinterpret_cast<PFN_vkDestroyShaderModule>(resolve("vkDestroyShaderModule"));
|
||||
fns.vkCreatePipelineLayout =
|
||||
reinterpret_cast<PFN_vkCreatePipelineLayout>(resolve("vkCreatePipelineLayout"));
|
||||
fns.vkDestroyPipelineLayout =
|
||||
reinterpret_cast<PFN_vkDestroyPipelineLayout>(resolve("vkDestroyPipelineLayout"));
|
||||
fns.vkCreateGraphicsPipelines =
|
||||
reinterpret_cast<PFN_vkCreateGraphicsPipelines>(resolve("vkCreateGraphicsPipelines"));
|
||||
fns.vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(resolve("vkDestroyPipeline"));
|
||||
fns.vkCmdBindPipeline = reinterpret_cast<PFN_vkCmdBindPipeline>(resolve("vkCmdBindPipeline"));
|
||||
fns.vkCmdDraw = reinterpret_cast<PFN_vkCmdDraw>(resolve("vkCmdDraw"));
|
||||
fns.vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(resolve("vkCreateFence"));
|
||||
fns.vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(resolve("vkDestroyFence"));
|
||||
fns.vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(resolve("vkQueueSubmit"));
|
||||
fns.vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(resolve("vkWaitForFences"));
|
||||
fns.vkGetQueryPoolResults =
|
||||
reinterpret_cast<PFN_vkGetQueryPoolResults>(resolve("vkGetQueryPoolResults"));
|
||||
fns.vkDeviceWaitIdle = reinterpret_cast<PFN_vkDeviceWaitIdle>(resolve("vkDeviceWaitIdle"));
|
||||
|
||||
const PrimitivesGeneratedNoXfbMeasurement measurement =
|
||||
RunPrimitivesGeneratedNoXfbProbe(probeContext);
|
||||
// Before any return below: the guard above owns the device and must know.
|
||||
probeFenceWaitTimedOut = measurement.fenceWaitTimedOut;
|
||||
if (!measurement.ran) {
|
||||
fail(format("the probe could not run ({}); the renderer's bring-up probe decides the "
|
||||
"reroute independently",
|
||||
measurement.failureReason));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto shapeFacts = [](const char* name,
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
if (!shape.drawn) {
|
||||
return format("{} not drawn (no tessellationShader)", name);
|
||||
}
|
||||
String facts = format("{}: stream answered {} of {} expected", name, shape.streamGenerated,
|
||||
shape.expectedPrimitives);
|
||||
if (shape.primitivesGeneratedExtMeasured) {
|
||||
facts += format(", dedicated query answered {}", shape.primitivesGeneratedExt);
|
||||
}
|
||||
if (shape.statisticsMeasured) {
|
||||
facts += format(", statistics control answered {}", shape.statisticsClippingInput);
|
||||
}
|
||||
if (!shape.primitivesGeneratedExtMeasured && !shape.statisticsMeasured) {
|
||||
facts += ", no control (neither VK_EXT_primitives_generated_query with its "
|
||||
"discard feature nor pipelineStatisticsQuery is available)";
|
||||
}
|
||||
return facts;
|
||||
};
|
||||
const String facts = shapeFacts("triangles", measurement.trianglesPlain) + "; " +
|
||||
shapeFacts("triangles under discard", measurement.trianglesDiscard) +
|
||||
"; " + shapeFacts("patches under discard", measurement.patchesDiscard);
|
||||
|
||||
const auto statisticsExactOn = [](const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
return shape.statisticsMeasured && shape.statisticsClippingInput == shape.expectedPrimitives;
|
||||
};
|
||||
// What the PLAIN-ONLY verdict actually measured, named from the numbers rather
|
||||
// than assumed: the shape the substitute misses may be the tessellated one
|
||||
// alone, and a missed shape may read a wrong NONZERO count rather than 0. A
|
||||
// row that always blamed rasterizer discard would put a false statement about
|
||||
// the driver into the campaign's evidence artifact, contradicted by the facts
|
||||
// string printed right after it.
|
||||
const auto describeMissedStatisticsShapes = [&]() {
|
||||
String missed;
|
||||
const auto note = [&](const char* name,
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
if (!shape.drawn || statisticsExactOn(shape)) {
|
||||
return;
|
||||
}
|
||||
if (!missed.empty()) {
|
||||
missed += " and ";
|
||||
}
|
||||
missed += name;
|
||||
missed += shape.statisticsMeasured
|
||||
? format(" (read {} of {} expected)", shape.statisticsClippingInput,
|
||||
shape.expectedPrimitives)
|
||||
: String(" (its statistics slot did not read back)");
|
||||
};
|
||||
note("the plain draw", measurement.trianglesPlain);
|
||||
note("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||
note("patches under rasterizer discard", measurement.patchesDiscard);
|
||||
return missed;
|
||||
};
|
||||
// The CTS's tessellator-measuring shape is a PATCHES draw under discard; say
|
||||
// whether THIS driver's substitute covers it instead of assuming it does not.
|
||||
const auto describeCtsShape = [&]() -> String {
|
||||
if (!measurement.patchesDiscard.drawn) {
|
||||
return "the CTS's tessellator-measuring shape (a PATCHES draw under discard) could "
|
||||
"not be measured here - this device has no tessellationShader - so whether "
|
||||
"the substitute covers it is unknown";
|
||||
}
|
||||
return statisticsExactOn(measurement.patchesDiscard)
|
||||
? "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||
"NOT among them: the substitute answers it exactly, so those tests are "
|
||||
"repaired"
|
||||
: "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||
"among them, so those tests stay broken on this driver";
|
||||
};
|
||||
|
||||
switch (EvaluatePrimitivesGeneratedNoXfbVerdict(measurement)) {
|
||||
case PrimitivesGeneratedNoXfbVerdict::StreamCounts:
|
||||
builder.Pass(RowName,
|
||||
"the stream query counts a draw made with no capture span open, as "
|
||||
"VK_EXT_transform_feedback defines (" +
|
||||
facts + ")");
|
||||
return;
|
||||
case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute:
|
||||
builder.Warn(RowName,
|
||||
"the stream query answers 0 for a draw made with no capture span open - "
|
||||
"the shape the CTS measures the tessellator with - while a "
|
||||
"VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT query around an identical replay answers "
|
||||
"exactly right, rasterizer discard included, so the renderer "
|
||||
"accumulates GL_PRIMITIVES_GENERATED for such draws through the "
|
||||
"dedicated query instead (one extra query slot per XFB-inactive draw "
|
||||
"inside a GENERATED span; " +
|
||||
facts + ")");
|
||||
return;
|
||||
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute:
|
||||
builder.Warn(RowName,
|
||||
"the stream query answers 0 for a draw made with no capture span open - "
|
||||
"the shape the CTS measures the tessellator with - while a "
|
||||
"clipping-invocations statistics query around an identical replay answers exactly "
|
||||
"right, rasterizer discard included, so the renderer accumulates "
|
||||
"GL_PRIMITIVES_GENERATED for such draws through a pipeline-statistics "
|
||||
"pool instead (one extra query slot per XFB-inactive draw inside a "
|
||||
"GENERATED span; " +
|
||||
facts + ")");
|
||||
return;
|
||||
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly:
|
||||
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||
"clipping-invocations statistics substitute counts the plain draw exactly but "
|
||||
"misses " +
|
||||
describeMissedStatisticsShapes() +
|
||||
" - each of them a shape the stream query answered 0 for as well, so the renderer "
|
||||
"reroutes XFB-inactive draws (repairing every shape the substitute answers, at no "
|
||||
"cost to the rest, which is what the verdict requires); " +
|
||||
describeCtsShape() + " (" + facts + ")");
|
||||
return;
|
||||
case PrimitivesGeneratedNoXfbVerdict::Unfixable: {
|
||||
// Two ways to land here, and the report must not conflate them: no
|
||||
// substitute answers even the plain draw, or one does but it is WRONG on a
|
||||
// shape the stream query answers EXACTLY - arming it would trade a correct
|
||||
// answer for a wrong one, so MobileGL refuses (see the verdict's
|
||||
// domination rule).
|
||||
String downgradeShapes;
|
||||
const auto noteDowngrade = [&](const char* name,
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
if (!shape.drawn || statisticsExactOn(shape) ||
|
||||
shape.streamGenerated != shape.expectedPrimitives) {
|
||||
return;
|
||||
}
|
||||
if (!downgradeShapes.empty()) {
|
||||
downgradeShapes += " and ";
|
||||
}
|
||||
downgradeShapes += name;
|
||||
};
|
||||
noteDowngrade("the plain draw", measurement.trianglesPlain);
|
||||
noteDowngrade("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||
noteDowngrade("patches under rasterizer discard", measurement.patchesDiscard);
|
||||
if (statisticsExactOn(measurement.trianglesPlain) && !downgradeShapes.empty()) {
|
||||
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||
"clipping-invocations statistics substitute repairs the plain draw but is wrong "
|
||||
"on " +
|
||||
downgradeShapes +
|
||||
", which the stream query answers exactly - rerouting every XFB-inactive draw "
|
||||
"would trade a correct count for a wrong one, so MobileGL arms nothing and the "
|
||||
"capture-less query keeps the driver's answers (" +
|
||||
facts + ")");
|
||||
return;
|
||||
}
|
||||
fail("the stream query answers 0 for a draw made with no capture span open and the "
|
||||
"device offers no working statistics substitute; an application sizing a capture "
|
||||
"buffer from GL_PRIMITIVES_GENERATED gets 0 (" +
|
||||
facts + ")");
|
||||
return;
|
||||
}
|
||||
case PrimitivesGeneratedNoXfbVerdict::Inconclusive:
|
||||
break;
|
||||
}
|
||||
fail("the probe reached no verdict - the answers fit neither the defect nor health, and "
|
||||
"MobileGL declines to repair a driver it does not understand (" +
|
||||
facts + ")");
|
||||
}
|
||||
|
||||
// Native iterationRP compute witness. This deliberately uses a separate
|
||||
// throwaway Vulkan device rather than the real renderer's queues, and it
|
||||
// treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
||||
// treats MOBILEGL_MAGMA_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
||||
// driver does, not what MobileGL elects to advertise to applications.
|
||||
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
@@ -2652,6 +3024,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||
properties, subgroupPropertiesAvailable, subgroupProperties);
|
||||
|
||||
ProbeVulkanPrimitivesGeneratedNoXfb(builder, getInstanceProcAddr, instance, physicalDevice,
|
||||
graphicsQueueFamilyIndex, deviceExtensions, features,
|
||||
vkGetPhysicalDeviceFeatures2Fn, vkGetPhysicalDeviceProperties2Fn);
|
||||
|
||||
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
|
||||
builder.Pass("VK_KHR_draw_indirect_count",
|
||||
"supported (count-buffer indirect draws run as single native "
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "PersistentBufferOrderingProbe.h"
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
using MG_External::GLESFunctionsTable;
|
||||
constexpr GLbitfield kPersistent = 0x0040;
|
||||
constexpr GLbitfield kCoherent = 0x0080;
|
||||
constexpr GLbitfield kDynamicStorage = 0x0100;
|
||||
constexpr GLbitfield kMapFlags = GL_MAP_WRITE_BIT | kPersistent | kCoherent;
|
||||
constexpr GLsizeiptr kArenaSize = 128 * 1024 * 1024;
|
||||
constexpr GLintptr kOffset = 96 * 1024 * 1024 + 28;
|
||||
constexpr GLsizei kSide = 128;
|
||||
constexpr GLsizei kSlots = 8;
|
||||
constexpr Int kBatches = 10;
|
||||
constexpr Int kDraws = 32;
|
||||
constexpr GLsizei kQuads = 64 * 32;
|
||||
constexpr Int kAttempts = 3;
|
||||
constexpr std::array<const char*, 3> kUploadNames = {"SubData", "Copy/persistent staging",
|
||||
"Copy/SubData staging"};
|
||||
enum class Shape { Unmapped, Mapped, FinishBefore, FinishBoth, MapThenUnmap, BarrierBefore };
|
||||
struct Vertex { GLfloat x, y, r, g, b; };
|
||||
constexpr GLsizeiptr kPayloadSize = kQuads * 6 * sizeof(Vertex);
|
||||
static_assert(kOffset + kPayloadSize <= kArenaSize);
|
||||
|
||||
void DrainErrors(const GLESFunctionsTable& gl) {
|
||||
for (Int i = 0; i < 32 && gl.glGetError() != GL_NO_ERROR; ++i) {}
|
||||
}
|
||||
|
||||
Bool CanProbe(const GLESFunctionsTable& gl) {
|
||||
if (!(gl.glGetIntegerv && gl.glGetBooleanv && gl.glGetFloatv && gl.glGetError &&
|
||||
gl.glGetStringi && gl.glIsEnabled && gl.glEnable && gl.glDisable &&
|
||||
gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
|
||||
gl.glGetShaderInfoLog && gl.glDeleteShader && gl.glCreateProgram && gl.glAttachShader &&
|
||||
gl.glLinkProgram && gl.glGetProgramiv && gl.glGetProgramInfoLog && gl.glDeleteProgram &&
|
||||
gl.glUseProgram && gl.glGenBuffers && gl.glBindBuffer && gl.glBufferStorageEXT &&
|
||||
gl.glMapBufferRange && gl.glUnmapBuffer && gl.glBufferData && gl.glBufferSubData &&
|
||||
gl.glCopyBufferSubData && gl.glDeleteBuffers && gl.glGenVertexArrays &&
|
||||
gl.glBindVertexArray && gl.glVertexAttribPointer && gl.glEnableVertexAttribArray &&
|
||||
gl.glDeleteVertexArrays && gl.glGenTextures && gl.glBindTexture && gl.glTexStorage2D &&
|
||||
gl.glDeleteTextures && gl.glGenFramebuffers && gl.glBindFramebuffer &&
|
||||
gl.glFramebufferTexture2D && gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers &&
|
||||
gl.glViewport && gl.glColorMask && gl.glClearColor && gl.glClear && gl.glDrawArrays &&
|
||||
gl.glFinish && gl.glMemoryBarrier && gl.glPixelStorei && gl.glReadPixels)) return false;
|
||||
DrainErrors(gl);
|
||||
GLint major = 0, minor = 0, count = 0;
|
||||
gl.glGetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
gl.glGetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
gl.glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
if (gl.glGetError() != GL_NO_ERROR || major < 3 || (major == 3 && minor < 1)) return false;
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const auto* extension = gl.glGetStringi(GL_EXTENSIONS, i);
|
||||
if (extension && std::strcmp(reinterpret_cast<const char*>(extension),
|
||||
"GL_EXT_buffer_storage") == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// This probe touches no images/SSBO bindings. Keep its scope independent from the
|
||||
// other POST probes, including pack state and the caller's currently active texture unit.
|
||||
struct StateScope {
|
||||
const GLESFunctionsTable& gl;
|
||||
GLint program = 0, vao = 0, array = 0, copyRead = 0, copyWrite = 0;
|
||||
GLint drawFbo = 0, readFbo = 0, texture = 0, packBuffer = 0;
|
||||
GLint viewport[4]{};
|
||||
GLfloat clear[4]{};
|
||||
GLboolean colorMask[4]{};
|
||||
static constexpr std::array<GLenum, 10> enables = {
|
||||
GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_CULL_FACE, GL_SCISSOR_TEST,
|
||||
GL_RASTERIZER_DISCARD, GL_DITHER, GL_SAMPLE_ALPHA_TO_COVERAGE,
|
||||
GL_SAMPLE_COVERAGE, GL_SAMPLE_MASK};
|
||||
static constexpr std::array<GLenum, 4> packNames = {
|
||||
GL_PACK_ALIGNMENT, GL_PACK_ROW_LENGTH, GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS};
|
||||
std::array<GLboolean, enables.size()> enabled{};
|
||||
std::array<GLint, packNames.size()> pack{};
|
||||
|
||||
explicit StateScope(const GLESFunctionsTable& api) : gl(api) {
|
||||
gl.glGetIntegerv(GL_CURRENT_PROGRAM, &program);
|
||||
gl.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao);
|
||||
gl.glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &array);
|
||||
gl.glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, ©Read);
|
||||
gl.glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, ©Write);
|
||||
gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo);
|
||||
gl.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFbo);
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture);
|
||||
gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer);
|
||||
gl.glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, clear);
|
||||
gl.glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
|
||||
for (SizeT i = 0; i < enables.size(); ++i) enabled[i] = gl.glIsEnabled(enables[i]);
|
||||
for (SizeT i = 0; i < packNames.size(); ++i) gl.glGetIntegerv(packNames[i], &pack[i]);
|
||||
}
|
||||
void Prepare() {
|
||||
for (auto cap : enables) gl.glDisable(cap);
|
||||
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
for (auto name : packNames) gl.glPixelStorei(name, name == GL_PACK_ALIGNMENT ? 1 : 0);
|
||||
gl.glViewport(0, 0, kSide, kSide);
|
||||
gl.glClearColor(0, 0, 0, 1);
|
||||
}
|
||||
~StateScope() {
|
||||
gl.glUseProgram(program);
|
||||
gl.glBindVertexArray(vao);
|
||||
gl.glBindBuffer(GL_ARRAY_BUFFER, array);
|
||||
gl.glBindBuffer(GL_COPY_READ_BUFFER, copyRead);
|
||||
gl.glBindBuffer(GL_COPY_WRITE_BUFFER, copyWrite);
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, packBuffer);
|
||||
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, drawFbo);
|
||||
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo);
|
||||
gl.glBindTexture(GL_TEXTURE_2D, texture);
|
||||
gl.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
gl.glClearColor(clear[0], clear[1], clear[2], clear[3]);
|
||||
gl.glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
|
||||
for (SizeT i = 0; i < enables.size(); ++i) {
|
||||
if (enabled[i]) gl.glEnable(enables[i]); else gl.glDisable(enables[i]);
|
||||
}
|
||||
for (SizeT i = 0; i < packNames.size(); ++i) gl.glPixelStorei(packNames[i], pack[i]);
|
||||
}
|
||||
};
|
||||
|
||||
struct Resources {
|
||||
const GLESFunctionsTable& gl;
|
||||
GLuint program = 0, vao = 0;
|
||||
std::array<GLuint, kSlots> fbos{}, textures{};
|
||||
explicit Resources(const GLESFunctionsTable& api) : gl(api) {}
|
||||
~Resources() {
|
||||
gl.glDeleteFramebuffers(kSlots, fbos.data());
|
||||
gl.glDeleteTextures(kSlots, textures.data());
|
||||
gl.glDeleteVertexArrays(1, &vao);
|
||||
if (program) gl.glDeleteProgram(program);
|
||||
}
|
||||
GLuint Compile(GLenum type, const char* source) {
|
||||
GLuint shader = gl.glCreateShader(type);
|
||||
if (!shader) return 0;
|
||||
gl.glShaderSource(shader, 1, &source, nullptr);
|
||||
gl.glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
char log[512]{};
|
||||
gl.glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
MGLOG_I("[driver-bug] persistent buffer ordering: shader failed: %s", log);
|
||||
gl.glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
Bool Setup() {
|
||||
const GLuint vs = Compile(GL_VERTEX_SHADER,
|
||||
"#version 310 es\nlayout(location=0) in vec2 pos; layout(location=1) in vec3 color;\n"
|
||||
"out highp vec3 vColor; void main(){gl_Position=vec4(pos,0,1);vColor=color;}\n");
|
||||
const GLuint fs = Compile(GL_FRAGMENT_SHADER,
|
||||
"#version 310 es\nprecision highp float; in highp vec3 vColor;\n"
|
||||
"layout(location=0) out vec4 outColor; void main(){outColor=vec4(vColor,1);}\n");
|
||||
if (vs && fs) {
|
||||
program = gl.glCreateProgram();
|
||||
if (program) {
|
||||
gl.glAttachShader(program, vs);
|
||||
gl.glAttachShader(program, fs);
|
||||
gl.glLinkProgram(program);
|
||||
}
|
||||
}
|
||||
if (vs) gl.glDeleteShader(vs);
|
||||
if (fs) gl.glDeleteShader(fs);
|
||||
if (!program) return false;
|
||||
GLint linked = 0;
|
||||
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[512]{};
|
||||
gl.glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
MGLOG_I("[driver-bug] persistent buffer ordering: link failed: %s", log);
|
||||
return false;
|
||||
}
|
||||
gl.glUseProgram(program);
|
||||
gl.glGenVertexArrays(1, &vao);
|
||||
gl.glBindVertexArray(vao);
|
||||
gl.glGenFramebuffers(kSlots, fbos.data());
|
||||
gl.glGenTextures(kSlots, textures.data());
|
||||
for (Int i = 0; i < kSlots; ++i) {
|
||||
if (!vao || !fbos[i] || !textures[i]) return false;
|
||||
gl.glBindTexture(GL_TEXTURE_2D, textures[i]);
|
||||
gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSide, kSide);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]);
|
||||
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
textures[i], 0);
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
|
||||
}
|
||||
return gl.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
};
|
||||
|
||||
struct Buffers {
|
||||
const GLESFunctionsTable& gl;
|
||||
GLuint arena = 0, staging = 0;
|
||||
explicit Buffers(const GLESFunctionsTable& api) : gl(api) {}
|
||||
~Buffers() {
|
||||
// All normal batches finish before cleanup; also retire a partially queued
|
||||
// batch on an error path before destroying a mapped staging source.
|
||||
gl.glFinish();
|
||||
gl.glDeleteBuffers(1, &arena);
|
||||
gl.glDeleteBuffers(1, &staging);
|
||||
}
|
||||
};
|
||||
|
||||
void FillVertices(Vector<Vertex>& vertices, Int channel) {
|
||||
constexpr std::array<Vertex, 6> quad = {{{-1,-1,0,0,0}, {1,-1,0,0,0}, {1,1,0,0,0},
|
||||
{-1,-1,0,0,0}, {1,1,0,0,0}, {-1,1,0,0,0}}};
|
||||
for (SizeT k = 0; k < vertices.size(); ++k) {
|
||||
auto& v = vertices[k];
|
||||
v = quad[k % 6];
|
||||
const SizeT q = k / 6;
|
||||
v.x = v.x / 64.f - 1.f + (2 * (q % 64) + 1) / 64.f;
|
||||
v.y = v.y / 32.f - 1.f + (2 * (q / 64) + 1) / 32.f;
|
||||
v.r = channel == 0 ? 1.f : 0.f;
|
||||
v.g = channel == 1 ? 1.f : 0.f;
|
||||
v.b = channel == 2 ? 1.f : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
BufferOrderingSample Run(const GLESFunctionsTable& gl, const Resources& resources,
|
||||
const Vector<Uint8>& seed, Int upload, Shape shape) {
|
||||
BufferOrderingSample sample;
|
||||
sample.status = BufferOrderingProbeStatus::Failed;
|
||||
Buffers buffers(gl);
|
||||
Vector<Vertex> payload(kQuads * 6);
|
||||
Vector<Uint8> pixels(kSide * kSide * 4);
|
||||
DrainErrors(gl);
|
||||
do {
|
||||
gl.glGenBuffers(1, &buffers.arena);
|
||||
if (!buffers.arena) break;
|
||||
gl.glBindBuffer(GL_ARRAY_BUFFER, buffers.arena);
|
||||
gl.glBufferStorageEXT(GL_ARRAY_BUFFER, kArenaSize, seed.data(), kMapFlags | kDynamicStorage);
|
||||
sample.error = gl.glGetError();
|
||||
if (sample.error != GL_NO_ERROR) break;
|
||||
if (shape != Shape::Unmapped) {
|
||||
// Deliberately never dereference the destination pointer. All destination
|
||||
// writes below are ordered GL commands, with no client mapping accesses.
|
||||
if (!gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, kArenaSize, kMapFlags)) break;
|
||||
if (shape == Shape::MapThenUnmap && !gl.glUnmapBuffer(GL_ARRAY_BUFFER)) break;
|
||||
}
|
||||
gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<const void*>(kOffset));
|
||||
gl.glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<const void*>(kOffset + 2 * sizeof(GLfloat)));
|
||||
gl.glEnableVertexAttribArray(0);
|
||||
gl.glEnableVertexAttribArray(1);
|
||||
void* sourceMap = nullptr;
|
||||
if (upload != 0) {
|
||||
gl.glGenBuffers(1, &buffers.staging);
|
||||
if (!buffers.staging) break;
|
||||
gl.glBindBuffer(GL_COPY_READ_BUFFER, buffers.staging);
|
||||
if (upload == 1) {
|
||||
gl.glBufferStorageEXT(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, kMapFlags);
|
||||
sourceMap = gl.glMapBufferRange(GL_COPY_READ_BUFFER, 0, kSlots * kPayloadSize, kMapFlags);
|
||||
if (!sourceMap) break;
|
||||
} else {
|
||||
gl.glBufferData(GL_COPY_READ_BUFFER, kSlots * kPayloadSize, nullptr, GL_STREAM_DRAW);
|
||||
}
|
||||
gl.glBindBuffer(GL_COPY_WRITE_BUFFER, buffers.arena);
|
||||
}
|
||||
sample.error = gl.glGetError();
|
||||
if (sample.error != GL_NO_ERROR) break;
|
||||
for (Int batch = 0; batch < kBatches; ++batch) {
|
||||
for (Int slot = 0; slot < kSlots; ++slot) {
|
||||
FillVertices(payload, (batch * kSlots + slot) % 3);
|
||||
if (shape == Shape::FinishBefore || shape == Shape::FinishBoth) gl.glFinish();
|
||||
if (shape == Shape::BarrierBefore) gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
if (upload == 0) {
|
||||
gl.glBufferSubData(GL_ARRAY_BUFFER, kOffset, kPayloadSize, payload.data());
|
||||
} else {
|
||||
// No slot is reused until the entire batch has finished on the GPU.
|
||||
if (upload == 1) {
|
||||
std::memcpy(static_cast<Uint8*>(sourceMap) + slot * kPayloadSize,
|
||||
payload.data(), kPayloadSize);
|
||||
} else {
|
||||
gl.glBufferSubData(GL_COPY_READ_BUFFER, slot * kPayloadSize,
|
||||
kPayloadSize, payload.data());
|
||||
}
|
||||
gl.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
slot * kPayloadSize, kOffset, kPayloadSize);
|
||||
}
|
||||
if (shape == Shape::FinishBoth) gl.glFinish();
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]);
|
||||
gl.glClear(GL_COLOR_BUFFER_BIT);
|
||||
for (Int draw = 0; draw < kDraws; ++draw) gl.glDrawArrays(GL_TRIANGLES, 0, kQuads * 6);
|
||||
}
|
||||
// No readback/Finish between subject update/draw pairs. Early readback
|
||||
// would hide precisely the old-reader/new-writer overlap being tested.
|
||||
gl.glFinish();
|
||||
sample.error = gl.glGetError();
|
||||
if (sample.error != GL_NO_ERROR) break;
|
||||
for (Int slot = 0; slot < kSlots; ++slot) {
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, resources.fbos[slot]);
|
||||
gl.glReadPixels(0, 0, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
sample.error = gl.glGetError();
|
||||
if (sample.error != GL_NO_ERROR) break;
|
||||
const Int channel = (batch * kSlots + slot) % 3;
|
||||
Uint bad = 0;
|
||||
for (Int pixel = 0; pixel < kSide * kSide; ++pixel) {
|
||||
for (Int c = 0; c < 3; ++c) {
|
||||
const Int expected = c == channel ? 255 : 0;
|
||||
if (std::abs(Int(pixels[pixel * 4 + c]) - expected) > 8) ++bad;
|
||||
}
|
||||
}
|
||||
++sample.frames;
|
||||
if (bad != 0) ++sample.badFrames;
|
||||
sample.badComponents += bad;
|
||||
}
|
||||
if (sample.error != GL_NO_ERROR) break;
|
||||
}
|
||||
if (sample.error == GL_NO_ERROR && sample.frames == kBatches * kSlots)
|
||||
sample.status = BufferOrderingProbeStatus::Complete;
|
||||
} while (false);
|
||||
if (sample.error == GL_NO_ERROR) sample.error = gl.glGetError();
|
||||
return sample;
|
||||
}
|
||||
|
||||
String Describe(const BufferOrderingSample& sample) {
|
||||
if (sample.status == BufferOrderingProbeStatus::NotRun) return "not run";
|
||||
if (sample.status == BufferOrderingProbeStatus::Failed)
|
||||
return format("inconclusive (GL error 0x{:x}, {} readbacks)", sample.error, sample.frames);
|
||||
return format("{}/{} bad FBOs ({} components)", sample.badFrames, sample.frames, sample.badComponents);
|
||||
}
|
||||
|
||||
String DescribeUpload(const BufferOrderingUploadMeasurement& row, Int upload) {
|
||||
return format("{}: mapped {}, never-mapped {}, Finish-before {}, Finish-both {}, "
|
||||
"map-then-unmap {}, barrier-before {}", kUploadNames[upload], Describe(row.mapped),
|
||||
Describe(row.unmapped), Describe(row.finishBefore), Describe(row.finishBoth),
|
||||
Describe(row.mapThenUnmap), Describe(row.barrierBefore));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering(const GLESFunctionsTable& gl) try {
|
||||
PersistentBufferOrderingMeasurement measurement;
|
||||
if (!CanProbe(gl)) return measurement;
|
||||
measurement.supported = true;
|
||||
StateScope state(gl);
|
||||
state.Prepare();
|
||||
Resources resources(gl);
|
||||
if (gl.glGetError() != GL_NO_ERROR || !resources.Setup()) {
|
||||
for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed;
|
||||
MGLOG_I("[driver-bug] persistent buffer ordering: setup failed; inconclusive");
|
||||
return measurement;
|
||||
}
|
||||
Vector<Uint8> seed(kArenaSize, 0);
|
||||
for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) {
|
||||
auto& row = measurement.uploads[upload];
|
||||
row.unmapped = Run(gl, resources, seed, upload, Shape::Unmapped);
|
||||
if (row.unmapped.Passed()) {
|
||||
// A single allocation can miss on Mali. Stop once a mismatch is measured,
|
||||
// otherwise retry with fresh storage rather than treating one pass as proof.
|
||||
for (Int attempt = 0; attempt < kAttempts; ++attempt) {
|
||||
const auto sample = Run(gl, resources, seed, upload, Shape::Mapped);
|
||||
row.mapped.status = sample.status;
|
||||
row.mapped.error = sample.error;
|
||||
row.mapped.frames += sample.frames;
|
||||
row.mapped.badFrames += sample.badFrames;
|
||||
row.mapped.badComponents += sample.badComponents;
|
||||
if (sample.status != BufferOrderingProbeStatus::Complete || sample.badFrames) break;
|
||||
}
|
||||
if (row.mapped.status == BufferOrderingProbeStatus::Complete && row.mapped.badFrames) {
|
||||
row.finishBoth = Run(gl, resources, seed, upload, Shape::FinishBoth);
|
||||
row.finishBefore = Run(gl, resources, seed, upload, Shape::FinishBefore);
|
||||
row.mapThenUnmap = Run(gl, resources, seed, upload, Shape::MapThenUnmap);
|
||||
row.barrierBefore = Run(gl, resources, seed, upload, Shape::BarrierBefore);
|
||||
}
|
||||
}
|
||||
MGLOG_I("[driver-bug] persistent buffer ordering: %s; %s", DescribeUpload(row, upload).c_str(),
|
||||
row.Detected() ? "detected" : "not detected or inconclusive");
|
||||
}
|
||||
return measurement;
|
||||
} catch (const std::bad_alloc&) {
|
||||
// The CPU initializer is arena-sized too. An allocation failure must not discard
|
||||
// the rest of the POST report or turn a partially sampled case into a finding.
|
||||
MGLOG_I("[driver-bug] persistent buffer ordering: host allocation failed; inconclusive");
|
||||
PersistentBufferOrderingMeasurement measurement;
|
||||
measurement.supported = true;
|
||||
for (auto& row : measurement.uploads) row.unmapped.status = BufferOrderingProbeStatus::Failed;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> DescribePersistentBufferOrderingBug(
|
||||
const PersistentBufferOrderingMeasurement& measurement) {
|
||||
String detail;
|
||||
for (Int upload = 0; upload < Int(measurement.uploads.size()); ++upload) {
|
||||
if (!measurement.uploads[upload].Detected()) continue;
|
||||
if (!detail.empty()) detail += "; ";
|
||||
detail += DescribeUpload(measurement.uploads[upload], upload);
|
||||
}
|
||||
if (detail.empty()) return std::nullopt;
|
||||
detail += ". A 128 MiB immutable vertex destination was mapped WRITE|PERSISTENT|COHERENT, "
|
||||
"but never accessed through its client pointer. Queued uploads/draws corrupt vertex data; "
|
||||
"identical never-mapped and Finish-before-and-after controls pass. "
|
||||
"This POST does not enable a workaround. MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1 "
|
||||
"avoids automatic arena adoption; explicit application mappings remain separate. "
|
||||
"FBO counts describe this bounded stress probe, not application flicker frequency.";
|
||||
return DriverBugFinding{"Persistent-mapped vertex buffers lose upload/draw ordering",
|
||||
DriverBugVerdict::Unfixable, Move(detail)};
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,66 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "DriverBugProbes.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
enum class BufferOrderingProbeStatus : Uint8 { NotRun, Complete, Failed };
|
||||
|
||||
struct BufferOrderingSample {
|
||||
BufferOrderingProbeStatus status = BufferOrderingProbeStatus::NotRun;
|
||||
Uint frames = 0; // Independent FBO readbacks, not draw calls or application frames.
|
||||
Uint badFrames = 0;
|
||||
Uint badComponents = 0;
|
||||
GLenum error = GL_NO_ERROR;
|
||||
|
||||
Bool Passed() const { return status == BufferOrderingProbeStatus::Complete && badFrames == 0; }
|
||||
};
|
||||
|
||||
struct BufferOrderingUploadMeasurement {
|
||||
BufferOrderingSample unmapped;
|
||||
BufferOrderingSample mapped;
|
||||
BufferOrderingSample finishBefore;
|
||||
BufferOrderingSample finishBoth;
|
||||
BufferOrderingSample mapThenUnmap;
|
||||
BufferOrderingSample barrierBefore;
|
||||
|
||||
Bool Detected() const {
|
||||
return unmapped.Passed() && finishBoth.Passed() &&
|
||||
mapped.status == BufferOrderingProbeStatus::Complete && mapped.badFrames != 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct PersistentBufferOrderingMeasurement {
|
||||
Bool supported = false;
|
||||
// SubData; CopyBufferSubData from coherent persistent staging; CopyBufferSubData
|
||||
// from ordinary SubData staging. Each has its OWN otherwise-identical controls.
|
||||
std::array<BufferOrderingUploadMeasurement, 3> uploads;
|
||||
};
|
||||
|
||||
// POST-only: native GLES calls, no MobileGL buffers, renderer-name rules or config changes.
|
||||
// The Mali r54p1 finding: updating an immutable vertex arena that has been persistently
|
||||
// mapped can corrupt queued draws even when the application never accesses that mapping.
|
||||
// Queue eight update/draw pairs into separate FBOs BEFORE any Finish/readback, then check
|
||||
// every pixel of both old and new draws. Staging slots never overlap while in flight.
|
||||
//
|
||||
// Each upload runs a never-mapped control with identical storage flags. Try up to three
|
||||
// fresh mapped allocations to catch intermittent failures. On corruption, measure explicit
|
||||
// waits, map-then-unmap and a barrier as diagnostics. Only a passing never-mapped AND
|
||||
// Finish-before-and-after control permits a finding. Setup/GL failures are inconclusive.
|
||||
// Explicit allocations are one 128 MiB arena, its initializer, and small staging/FBOs;
|
||||
// allocations, batches and draws are bounded. Every touched GL state is restored.
|
||||
PersistentBufferOrderingMeasurement ProbePersistentBufferUpdateOrdering(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Used by the POST collector. A report never labels an inconclusive sample as a bug.
|
||||
Optional<DriverBugFinding> DescribePersistentBufferOrderingBug(
|
||||
const PersistentBufferOrderingMeasurement& measurement);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,556 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.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
|
||||
//
|
||||
// See the header for what is being measured and why. The plumbing here is shaped
|
||||
// like the POST timestamp probe (DriverPost.cpp, ProbeVulkanTimerQuery): one
|
||||
// throwaway command buffer, a bounded fence wait that deliberately leaks the
|
||||
// device objects rather than idle-wait a hung GPU, and teardown on every path.
|
||||
|
||||
#include "PrimitivesGeneratedNoXfbProbe.h"
|
||||
#include "PrimitivesGeneratedNoXfbProbeSpv.h"
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
template <typename Callable>
|
||||
struct ProbeScopeGuard {
|
||||
explicit ProbeScopeGuard(Callable callable) : onExit(Move(callable)) {}
|
||||
ProbeScopeGuard(const ProbeScopeGuard&) = delete;
|
||||
ProbeScopeGuard& operator=(const ProbeScopeGuard&) = delete;
|
||||
~ProbeScopeGuard() { onExit(); }
|
||||
|
||||
private:
|
||||
Callable onExit;
|
||||
};
|
||||
|
||||
Bool AllRequiredFnsPresent(const PrimitivesGeneratedNoXfbProbeFns& fns) {
|
||||
return fns.vkCreateCommandPool != nullptr && fns.vkDestroyCommandPool != nullptr &&
|
||||
fns.vkAllocateCommandBuffers != nullptr && fns.vkBeginCommandBuffer != nullptr &&
|
||||
fns.vkEndCommandBuffer != nullptr && fns.vkCreateQueryPool != nullptr &&
|
||||
fns.vkDestroyQueryPool != nullptr && fns.vkCmdResetQueryPool != nullptr &&
|
||||
fns.vkCmdBeginQuery != nullptr && fns.vkCmdEndQuery != nullptr &&
|
||||
fns.vkCmdBeginQueryIndexedEXT != nullptr && fns.vkCmdEndQueryIndexedEXT != nullptr &&
|
||||
fns.vkCreateRenderPass != nullptr && fns.vkDestroyRenderPass != nullptr &&
|
||||
fns.vkCreateFramebuffer != nullptr && fns.vkDestroyFramebuffer != nullptr &&
|
||||
fns.vkCmdBeginRenderPass != nullptr && fns.vkCmdEndRenderPass != nullptr &&
|
||||
fns.vkCreateShaderModule != nullptr && fns.vkDestroyShaderModule != nullptr &&
|
||||
fns.vkCreatePipelineLayout != nullptr && fns.vkDestroyPipelineLayout != nullptr &&
|
||||
fns.vkCreateGraphicsPipelines != nullptr && fns.vkDestroyPipeline != nullptr &&
|
||||
fns.vkCmdBindPipeline != nullptr && fns.vkCmdDraw != nullptr &&
|
||||
fns.vkCreateFence != nullptr && fns.vkDestroyFence != nullptr &&
|
||||
fns.vkQueueSubmit != nullptr && fns.vkWaitForFences != nullptr &&
|
||||
fns.vkGetQueryPoolResults != nullptr && fns.vkDeviceWaitIdle != nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe(
|
||||
const PrimitivesGeneratedNoXfbProbeContext& context) {
|
||||
PrimitivesGeneratedNoXfbMeasurement measurement;
|
||||
const auto fail = [&](String reason) {
|
||||
measurement.ran = false;
|
||||
measurement.failureReason = Move(reason);
|
||||
return measurement;
|
||||
};
|
||||
|
||||
if (!context.transformFeedbackQueriesUsable) {
|
||||
return fail("transform feedback stream queries are not usable on this device, so the "
|
||||
"probe has no subject");
|
||||
}
|
||||
if (context.device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) {
|
||||
return fail("no device/queue was supplied");
|
||||
}
|
||||
const PrimitivesGeneratedNoXfbProbeFns& fns = context.fns;
|
||||
if (!AllRequiredFnsPresent(fns)) {
|
||||
return fail("a required Vulkan entry point was not resolved");
|
||||
}
|
||||
|
||||
VkDevice device = context.device;
|
||||
|
||||
// Slot i of each pool belongs to shape i (0 = triangles plain, 1 = triangles
|
||||
// under discard, 2 = patches under discard). Unused slots are reset either
|
||||
// way; reset needs no feature and an unqueried reset slot is never read.
|
||||
constexpr Uint32 kShapeSlots = 3;
|
||||
const Bool drawPatches = context.tessellationEnabled;
|
||||
const Bool measureStatistics = context.pipelineStatisticsEnabled;
|
||||
// Only with BOTH feature bits: without ...WithRasterizerDiscard, a
|
||||
// discarding draw inside the query is invalid usage
|
||||
// (VUID-vkCmdDraw-primitivesGeneratedQueryWithRasterizerDiscard-06708),
|
||||
// and two of the three shapes discard.
|
||||
const Bool measurePrimitivesGeneratedExt = context.primitivesGeneratedQueryUsable;
|
||||
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkQueryPool streamQueryPool = VK_NULL_HANDLE;
|
||||
VkQueryPool primitivesGeneratedQueryPool = VK_NULL_HANDLE;
|
||||
VkQueryPool statisticsQueryPool = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkShaderModule vertModule = VK_NULL_HANDLE;
|
||||
VkShaderModule tescModule = VK_NULL_HANDLE;
|
||||
VkShaderModule teseModule = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkPipeline trianglePlainPipeline = VK_NULL_HANDLE;
|
||||
VkPipeline triangleDiscardPipeline = VK_NULL_HANDLE;
|
||||
VkPipeline patchDiscardPipeline = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
|
||||
// Teardown on every path. When the fence wait timed out the submission may
|
||||
// still be executing on a hung GPU: vkDeviceWaitIdle could block forever
|
||||
// and destroying in-flight objects is undefined, so everything is
|
||||
// deliberately leaked - a hung GPU must not hang the caller. The same flag
|
||||
// is returned in the measurement, because a caller that OWNS the device must
|
||||
// make the same choice for it (see the header): destroying a device whose
|
||||
// children are alive and whose queue may still be executing is the very hang
|
||||
// this bound exists to prevent.
|
||||
const ProbeScopeGuard teardown([&]() {
|
||||
if (measurement.fenceWaitTimedOut) {
|
||||
return;
|
||||
}
|
||||
fns.vkDeviceWaitIdle(device);
|
||||
if (fence != VK_NULL_HANDLE) fns.vkDestroyFence(device, fence, nullptr);
|
||||
if (trianglePlainPipeline != VK_NULL_HANDLE)
|
||||
fns.vkDestroyPipeline(device, trianglePlainPipeline, nullptr);
|
||||
if (triangleDiscardPipeline != VK_NULL_HANDLE)
|
||||
fns.vkDestroyPipeline(device, triangleDiscardPipeline, nullptr);
|
||||
if (patchDiscardPipeline != VK_NULL_HANDLE)
|
||||
fns.vkDestroyPipeline(device, patchDiscardPipeline, nullptr);
|
||||
if (pipelineLayout != VK_NULL_HANDLE) fns.vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
||||
if (vertModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, vertModule, nullptr);
|
||||
if (tescModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, tescModule, nullptr);
|
||||
if (teseModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, teseModule, nullptr);
|
||||
if (framebuffer != VK_NULL_HANDLE) fns.vkDestroyFramebuffer(device, framebuffer, nullptr);
|
||||
if (renderPass != VK_NULL_HANDLE) fns.vkDestroyRenderPass(device, renderPass, nullptr);
|
||||
if (statisticsQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, statisticsQueryPool, nullptr);
|
||||
if (primitivesGeneratedQueryPool != VK_NULL_HANDLE)
|
||||
fns.vkDestroyQueryPool(device, primitivesGeneratedQueryPool, nullptr);
|
||||
if (streamQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, streamQueryPool, nullptr);
|
||||
if (commandPool != VK_NULL_HANDLE) fns.vkDestroyCommandPool(device, commandPool, nullptr);
|
||||
});
|
||||
|
||||
VkCommandPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
|
||||
if (fns.vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
|
||||
return fail("vkCreateCommandPool failed");
|
||||
}
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
if (fns.vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
|
||||
return fail("vkAllocateCommandBuffers failed");
|
||||
}
|
||||
|
||||
VkQueryPoolCreateInfo streamPoolInfo{};
|
||||
streamPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
streamPoolInfo.queryType = VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT;
|
||||
streamPoolInfo.queryCount = kShapeSlots;
|
||||
if (fns.vkCreateQueryPool(device, &streamPoolInfo, nullptr, &streamQueryPool) != VK_SUCCESS) {
|
||||
return fail("vkCreateQueryPool(TRANSFORM_FEEDBACK_STREAM) failed");
|
||||
}
|
||||
if (measurePrimitivesGeneratedExt) {
|
||||
VkQueryPoolCreateInfo pgqPoolInfo{};
|
||||
pgqPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
pgqPoolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT;
|
||||
pgqPoolInfo.queryCount = kShapeSlots;
|
||||
if (fns.vkCreateQueryPool(device, &pgqPoolInfo, nullptr, &primitivesGeneratedQueryPool) !=
|
||||
VK_SUCCESS) {
|
||||
return fail("vkCreateQueryPool(PRIMITIVES_GENERATED_EXT) failed");
|
||||
}
|
||||
}
|
||||
if (measureStatistics) {
|
||||
VkQueryPoolCreateInfo statPoolInfo{};
|
||||
statPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
statPoolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
|
||||
statPoolInfo.queryCount = kShapeSlots;
|
||||
// CLIPPING_INVOCATIONS counts the primitives PROCESSED BY (i.e. reaching)
|
||||
// primitive clipping - GL's CLIPPING_INPUT_PRIMITIVES - which is the
|
||||
// pre-clip, post-vertex-processing set PRIMITIVES_GENERATED is defined
|
||||
// over. CLIPPING_PRIMITIVES (the stage's OUTPUT count) would be wrong:
|
||||
// clipping may drop or split primitives.
|
||||
statPoolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT;
|
||||
if (fns.vkCreateQueryPool(device, &statPoolInfo, nullptr, &statisticsQueryPool) != VK_SUCCESS) {
|
||||
return fail("vkCreateQueryPool(PIPELINE_STATISTICS) failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Zero-attachment render pass + 1x1 framebuffer: the draw is discarded
|
||||
// before rasterization, nothing is ever written, but vkCmdDraw needs a
|
||||
// render pass instance to live in.
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
VkRenderPassCreateInfo renderPassInfo{};
|
||||
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||
renderPassInfo.subpassCount = 1;
|
||||
renderPassInfo.pSubpasses = &subpass;
|
||||
if (fns.vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
|
||||
return fail("vkCreateRenderPass failed");
|
||||
}
|
||||
VkFramebufferCreateInfo framebufferInfo{};
|
||||
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||
framebufferInfo.renderPass = renderPass;
|
||||
framebufferInfo.width = 1;
|
||||
framebufferInfo.height = 1;
|
||||
framebufferInfo.layers = 1;
|
||||
if (fns.vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) {
|
||||
return fail("vkCreateFramebuffer failed");
|
||||
}
|
||||
|
||||
const auto makeModule = [&](const std::uint32_t* words, std::size_t wordCount, VkShaderModule& out) {
|
||||
VkShaderModuleCreateInfo moduleInfo{};
|
||||
moduleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||
moduleInfo.codeSize = wordCount * sizeof(std::uint32_t);
|
||||
moduleInfo.pCode = words;
|
||||
return fns.vkCreateShaderModule(device, &moduleInfo, nullptr, &out) == VK_SUCCESS;
|
||||
};
|
||||
if (!makeModule(kPrimitivesGeneratedNoXfbProbeVertSpv, kPrimitivesGeneratedNoXfbProbeVertSpvWordCount,
|
||||
vertModule)) {
|
||||
return fail("vkCreateShaderModule(vert) failed");
|
||||
}
|
||||
if (drawPatches) {
|
||||
if (!makeModule(kPrimitivesGeneratedNoXfbProbeTescSpv, kPrimitivesGeneratedNoXfbProbeTescSpvWordCount,
|
||||
tescModule) ||
|
||||
!makeModule(kPrimitivesGeneratedNoXfbProbeTeseSpv, kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount,
|
||||
teseModule)) {
|
||||
return fail("vkCreateShaderModule(tesc/tese) failed");
|
||||
}
|
||||
}
|
||||
|
||||
VkPipelineLayoutCreateInfo layoutInfo{};
|
||||
layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
if (fns.vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
|
||||
return fail("vkCreatePipelineLayout failed");
|
||||
}
|
||||
|
||||
// With rasterizerDiscardEnable the viewport and multisample state are
|
||||
// ignored by the spec, but well-formed ones are supplied anyway: the probe
|
||||
// must never be the thing that trips a picky driver. The discard-off
|
||||
// variant rasterizes into the zero-attachment subpass, which writes
|
||||
// nothing anywhere.
|
||||
const auto makePipeline = [&](Bool tessellated, Bool rasterizerDiscard, VkPipeline& out) {
|
||||
VkPipelineShaderStageCreateInfo stages[3] = {};
|
||||
Uint32 stageCount = 0;
|
||||
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stages[stageCount].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[stageCount].module = vertModule;
|
||||
stages[stageCount].pName = "main";
|
||||
++stageCount;
|
||||
if (tessellated) {
|
||||
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||
stages[stageCount].module = tescModule;
|
||||
stages[stageCount].pName = "main";
|
||||
++stageCount;
|
||||
stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;
|
||||
stages[stageCount].module = teseModule;
|
||||
stages[stageCount].pName = "main";
|
||||
++stageCount;
|
||||
}
|
||||
|
||||
VkPipelineVertexInputStateCreateInfo vertexInput{};
|
||||
vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
||||
|
||||
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
|
||||
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
||||
inputAssembly.topology =
|
||||
tessellated ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
|
||||
VkPipelineTessellationStateCreateInfo tessellation{};
|
||||
tessellation.sType = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO;
|
||||
tessellation.patchControlPoints = 1;
|
||||
|
||||
VkViewport viewport{};
|
||||
viewport.width = 1.0f;
|
||||
viewport.height = 1.0f;
|
||||
viewport.maxDepth = 1.0f;
|
||||
VkRect2D scissor{};
|
||||
scissor.extent.width = 1;
|
||||
scissor.extent.height = 1;
|
||||
VkPipelineViewportStateCreateInfo viewportState{};
|
||||
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
|
||||
viewportState.viewportCount = 1;
|
||||
viewportState.pViewports = &viewport;
|
||||
viewportState.scissorCount = 1;
|
||||
viewportState.pScissors = &scissor;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo rasterization{};
|
||||
rasterization.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
||||
rasterization.rasterizerDiscardEnable = rasterizerDiscard ? VK_TRUE : VK_FALSE;
|
||||
rasterization.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
rasterization.cullMode = VK_CULL_MODE_NONE;
|
||||
rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
rasterization.lineWidth = 1.0f;
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo multisample{};
|
||||
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
||||
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
|
||||
VkGraphicsPipelineCreateInfo pipelineInfo{};
|
||||
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
||||
pipelineInfo.stageCount = stageCount;
|
||||
pipelineInfo.pStages = stages;
|
||||
pipelineInfo.pVertexInputState = &vertexInput;
|
||||
pipelineInfo.pInputAssemblyState = &inputAssembly;
|
||||
pipelineInfo.pTessellationState = tessellated ? &tessellation : nullptr;
|
||||
pipelineInfo.pViewportState = &viewportState;
|
||||
pipelineInfo.pRasterizationState = &rasterization;
|
||||
pipelineInfo.pMultisampleState = &multisample;
|
||||
pipelineInfo.layout = pipelineLayout;
|
||||
pipelineInfo.renderPass = renderPass;
|
||||
pipelineInfo.subpass = 0;
|
||||
return fns.vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &out) ==
|
||||
VK_SUCCESS;
|
||||
};
|
||||
if (!makePipeline(false, false, trianglePlainPipeline)) {
|
||||
return fail("vkCreateGraphicsPipelines(triangles) failed");
|
||||
}
|
||||
if (!makePipeline(false, true, triangleDiscardPipeline)) {
|
||||
return fail("vkCreateGraphicsPipelines(triangles, discard) failed");
|
||||
}
|
||||
if (drawPatches && !makePipeline(true, true, patchDiscardPipeline)) {
|
||||
return fail("vkCreateGraphicsPipelines(patches, discard) failed");
|
||||
}
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if (fns.vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
|
||||
return fail("vkBeginCommandBuffer failed");
|
||||
}
|
||||
fns.vkCmdResetQueryPool(commandBuffer, streamQueryPool, 0, kShapeSlots);
|
||||
if (measurePrimitivesGeneratedExt) {
|
||||
fns.vkCmdResetQueryPool(commandBuffer, primitivesGeneratedQueryPool, 0, kShapeSlots);
|
||||
}
|
||||
if (measureStatistics) {
|
||||
fns.vkCmdResetQueryPool(commandBuffer, statisticsQueryPool, 0, kShapeSlots);
|
||||
}
|
||||
|
||||
VkRenderPassBeginInfo renderPassBegin{};
|
||||
renderPassBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||
renderPassBegin.renderPass = renderPass;
|
||||
renderPassBegin.framebuffer = framebuffer;
|
||||
renderPassBegin.renderArea.extent.width = 1;
|
||||
renderPassBegin.renderArea.extent.height = 1;
|
||||
fns.vkCmdBeginRenderPass(commandBuffer, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
// Each query kind wraps ITS OWN replay of the shape's draw, never a shared
|
||||
// one. Not pedantry - a co-active control CONTAMINATES the subject:
|
||||
// measured on lavapipe, a dedicated primitives-generated query active
|
||||
// around the same draw switches llvmpipe's primitive collection on, and
|
||||
// the stream query on that draw then answers the exact count it answers 0
|
||||
// for when it is alone - which is how the renderer actually runs it. A
|
||||
// probe that measured them together certified this driver healthy and
|
||||
// repaired nothing. The replays are identical recordings of a
|
||||
// deterministic draw, so the per-shape comparison loses nothing.
|
||||
const auto recordShape = [&](Uint32 slot, VkPipeline pipeline, Uint32 vertexCount) {
|
||||
fns.vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
// THE SUBJECT, alone: no vkCmdBeginTransformFeedbackEXT anywhere in
|
||||
// this command buffer - the stream query wraps a draw with transform
|
||||
// feedback inactive, exactly the CTS's tessellator-measuring shape.
|
||||
fns.vkCmdBeginQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0, 0);
|
||||
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||
fns.vkCmdEndQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0);
|
||||
if (measurePrimitivesGeneratedExt) {
|
||||
// Plain vkCmdBeginQuery: a PRIMITIVES_GENERATED_EXT query begun
|
||||
// this way counts vertex stream 0, which is where every non-GS
|
||||
// (and default-stream GS) primitive goes.
|
||||
fns.vkCmdBeginQuery(commandBuffer, primitivesGeneratedQueryPool, slot, 0);
|
||||
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||
fns.vkCmdEndQuery(commandBuffer, primitivesGeneratedQueryPool, slot);
|
||||
}
|
||||
if (measureStatistics) {
|
||||
fns.vkCmdBeginQuery(commandBuffer, statisticsQueryPool, slot, 0);
|
||||
fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
|
||||
fns.vkCmdEndQuery(commandBuffer, statisticsQueryPool, slot);
|
||||
}
|
||||
};
|
||||
recordShape(0, trianglePlainPipeline, 3); // one rasterized triangle
|
||||
recordShape(1, triangleDiscardPipeline, 3); // one discarded triangle
|
||||
if (drawPatches) {
|
||||
// one 1-vertex patch -> one tessellated, discarded triangle
|
||||
recordShape(2, patchDiscardPipeline, 1);
|
||||
}
|
||||
fns.vkCmdEndRenderPass(commandBuffer);
|
||||
if (fns.vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
|
||||
return fail("vkEndCommandBuffer failed");
|
||||
}
|
||||
|
||||
VkFenceCreateInfo fenceInfo{};
|
||||
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
if (fns.vkCreateFence(device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
|
||||
return fail("vkCreateFence failed");
|
||||
}
|
||||
VkSubmitInfo submitInfo{};
|
||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &commandBuffer;
|
||||
if (fns.vkQueueSubmit(context.queue, 1, &submitInfo, fence) != VK_SUCCESS) {
|
||||
return fail("vkQueueSubmit failed");
|
||||
}
|
||||
constexpr Uint64 kFenceTimeoutNs = 5'000'000'000ull; // a probe must never hang its caller
|
||||
if (fns.vkWaitForFences(device, 1, &fence, VK_TRUE, kFenceTimeoutNs) != VK_SUCCESS) {
|
||||
// Set BEFORE failing: the scope guard reads it to skip every destroy, and
|
||||
// the caller reads it out of the measurement to skip destroying the device.
|
||||
measurement.fenceWaitTimedOut = true;
|
||||
return fail("the probe submission did not complete within 5 s");
|
||||
}
|
||||
|
||||
const auto readShape = [&](Uint32 slot, Uint64 expected, PrimitivesGeneratedNoXfbShapeMeasurement& out) {
|
||||
Uint64 streamPair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||
if (fns.vkGetQueryPoolResults(device, streamQueryPool, slot, 1, sizeof(streamPair), streamPair,
|
||||
sizeof(streamPair),
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
out.drawn = true;
|
||||
out.expectedPrimitives = expected;
|
||||
out.streamGenerated = streamPair[1];
|
||||
if (measurePrimitivesGeneratedExt) {
|
||||
Uint64 generated = 0;
|
||||
if (fns.vkGetQueryPoolResults(device, primitivesGeneratedQueryPool, slot, 1, sizeof(generated),
|
||||
&generated, sizeof(generated),
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) {
|
||||
out.primitivesGeneratedExtMeasured = true;
|
||||
out.primitivesGeneratedExt = generated;
|
||||
}
|
||||
}
|
||||
if (measureStatistics) {
|
||||
Uint64 clippingInput = 0;
|
||||
if (fns.vkGetQueryPoolResults(device, statisticsQueryPool, slot, 1, sizeof(clippingInput),
|
||||
&clippingInput, sizeof(clippingInput),
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) {
|
||||
out.statisticsMeasured = true;
|
||||
out.statisticsClippingInput = clippingInput;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!readShape(0, 1, measurement.trianglesPlain)) {
|
||||
return fail("vkGetQueryPoolResults(triangles) failed");
|
||||
}
|
||||
if (!readShape(1, 1, measurement.trianglesDiscard)) {
|
||||
return fail("vkGetQueryPoolResults(triangles, discard) failed");
|
||||
}
|
||||
if (drawPatches && !readShape(2, 1, measurement.patchesDiscard)) {
|
||||
return fail("vkGetQueryPoolResults(patches, discard) failed");
|
||||
}
|
||||
|
||||
measurement.ran = true;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
const PrimitivesGeneratedNoXfbMeasurement& measurement) {
|
||||
if (!measurement.ran || !measurement.trianglesPlain.drawn || !measurement.trianglesDiscard.drawn) {
|
||||
return PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||
}
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement* shapes[3] = {&measurement.trianglesPlain,
|
||||
&measurement.trianglesDiscard,
|
||||
&measurement.patchesDiscard};
|
||||
Bool anyStreamSilent = false;
|
||||
Bool allStreamExact = true;
|
||||
Bool allPrimitivesGeneratedExtExact = true;
|
||||
Bool allStatisticsExact = true;
|
||||
// Whether the statistics substitute DOMINATES the stream query shape by shape:
|
||||
// every shape the statistics do not answer exactly must be one the stream query
|
||||
// answered 0 for anyway. Without this, a plain-shape-only substitute could be
|
||||
// armed on a device whose stream query was RIGHT on a shape the statistics get
|
||||
// wrong - and the renderer reroutes every XFB-inactive draw, so that shape would
|
||||
// be downgraded from correct to wrong. "Never worse per draw" is what makes
|
||||
// arming on an uncharacterised driver defensible; it has to be measured, not
|
||||
// assumed.
|
||||
Bool statisticsDominateStream = true;
|
||||
for (const auto* shape : shapes) {
|
||||
if (!shape->drawn) {
|
||||
continue;
|
||||
}
|
||||
if (shape->streamGenerated == 0) {
|
||||
anyStreamSilent = true;
|
||||
}
|
||||
if (shape->streamGenerated != shape->expectedPrimitives) {
|
||||
allStreamExact = false;
|
||||
// A nonzero wrong answer is neither the defect nor health: refuse
|
||||
// a verdict rather than repair a driver the probe does not
|
||||
// understand.
|
||||
if (shape->streamGenerated != 0) {
|
||||
return PrimitivesGeneratedNoXfbVerdict::Inconclusive;
|
||||
}
|
||||
}
|
||||
if (!shape->primitivesGeneratedExtMeasured ||
|
||||
shape->primitivesGeneratedExt != shape->expectedPrimitives) {
|
||||
allPrimitivesGeneratedExtExact = false;
|
||||
}
|
||||
if (!shape->statisticsMeasured ||
|
||||
shape->statisticsClippingInput != shape->expectedPrimitives) {
|
||||
allStatisticsExact = false;
|
||||
// Only a shape the stream query was silent on may be left behind by
|
||||
// the substitute; a shape it answered exactly must not be traded away.
|
||||
if (shape->streamGenerated == shape->expectedPrimitives) {
|
||||
statisticsDominateStream = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allStreamExact) {
|
||||
return PrimitivesGeneratedNoXfbVerdict::StreamCounts;
|
||||
}
|
||||
// At this point at least one drawn shape answered exactly 0.
|
||||
MOBILEGL_ASSERT(anyStreamSilent, "verdict fell through with no silent shape");
|
||||
if (allPrimitivesGeneratedExtExact) {
|
||||
return PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute;
|
||||
}
|
||||
if (allStatisticsExact) {
|
||||
return PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute;
|
||||
}
|
||||
const auto& plain = measurement.trianglesPlain;
|
||||
const Bool plainStatisticsExact =
|
||||
plain.statisticsMeasured && plain.statisticsClippingInput == plain.expectedPrimitives;
|
||||
// Both halves are required: the substitute must repair the plain shape, AND it
|
||||
// must not cost any shape an answer the stream query already had right.
|
||||
return (plainStatisticsExact && statisticsDominateStream)
|
||||
? PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly
|
||||
: PrimitivesGeneratedNoXfbVerdict::Unfixable;
|
||||
}
|
||||
|
||||
PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting,
|
||||
PrimitivesGeneratedNoXfbVerdict verdict,
|
||||
Bool primitivesGeneratedQueryUsable,
|
||||
Bool pipelineStatisticsEnabled) {
|
||||
switch (overrideSetting) {
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
return PrimGenRerouteKind::None;
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
// ForceOn bypasses the device verdict, never the structural checks:
|
||||
// without a hostable pool there is nothing to route through. The
|
||||
// dedicated query wins where both exist - its semantics are the GL
|
||||
// target's by definition.
|
||||
if (primitivesGeneratedQueryUsable) {
|
||||
return PrimGenRerouteKind::PrimitivesGeneratedExt;
|
||||
}
|
||||
return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics
|
||||
: PrimGenRerouteKind::None;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
switch (verdict) {
|
||||
case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute:
|
||||
return primitivesGeneratedQueryUsable ? PrimGenRerouteKind::PrimitivesGeneratedExt
|
||||
: PrimGenRerouteKind::None;
|
||||
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute:
|
||||
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly:
|
||||
return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics
|
||||
: PrimGenRerouteKind::None;
|
||||
case PrimitivesGeneratedNoXfbVerdict::Inconclusive:
|
||||
case PrimitivesGeneratedNoXfbVerdict::StreamCounts:
|
||||
case PrimitivesGeneratedNoXfbVerdict::Unfixable:
|
||||
break;
|
||||
}
|
||||
return PrimGenRerouteKind::None;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,258 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Config.h>
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
// ============ PRIMITIVES GENERATED WITHOUT TRANSFORM FEEDBACK ============
|
||||
//
|
||||
// GL_PRIMITIVES_GENERATED counts what the last vertex processing stage emits
|
||||
// whether or not a transform feedback capture is active (GL 4.6 core 13.4), and
|
||||
// the DirectVulkan backend serves it from the second result
|
||||
// (primitivesNeeded) of a VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT pool
|
||||
// slot wrapped around each draw. VK_EXT_transform_feedback defines that value
|
||||
// as the primitives the vertex stream produced, capture or no capture - but a
|
||||
// Mali driver (G1-Ultra, observed against the gl44/gl45/gl46 CTS) answers 0
|
||||
// for every draw made while no vkCmdBeginTransformFeedbackEXT span is open,
|
||||
// while answering exactly right as soon as one is. The tessellation suites
|
||||
// measure the tessellator by exactly that shape (rasterizer discard on,
|
||||
// transform feedback INACTIVE, a PATCHES draw inside a GENERATED query;
|
||||
// esextcTessellationShaderUtils.cpp, captureTessellationData), size their
|
||||
// capture buffers from the answer, and die on the zero-byte buffer the 0
|
||||
// produces - about 29 tessellation tests per tree plus all 13
|
||||
// tessellation_shader.vertex bodies.
|
||||
//
|
||||
// THE PROBE draws three shapes through pipelines with no Xfb execution mode
|
||||
// and no transform feedback begun, each inside its own stream-query slot:
|
||||
// - one triangle, plainly (no rasterizer discard);
|
||||
// - one triangle with rasterizer discard baked into the pipeline;
|
||||
// - one PATCHES draw with discard, through a passthrough tessellation
|
||||
// pipeline whose all-1 levels emit exactly one triangle (when the device
|
||||
// has tessellationShader) - the CTS shape verbatim.
|
||||
// Alongside each stream slot it measures the TWO candidate substitutes, each
|
||||
// around ITS OWN identical replay of the shape's draw - never co-active with
|
||||
// the subject, because a co-active control contaminates it: on lavapipe a
|
||||
// dedicated primitives-generated query active around the same draw switches
|
||||
// the driver's primitive collection on and the stream query then counts a
|
||||
// draw it answers 0 for when alone, which is how the renderer actually runs
|
||||
// it. The substitutes:
|
||||
// - a VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT slot, where the device has
|
||||
// VK_EXT_primitives_generated_query with BOTH primitivesGeneratedQuery and
|
||||
// primitivesGeneratedQueryWithRasterizerDiscard (without the discard
|
||||
// feature the spec forbids the query around a discarding draw at all -
|
||||
// VUID-vkCmdDraw-...-06708 - and GL applications toggle discard freely, so
|
||||
// a base-feature-only device cannot use this tier). The extension exists
|
||||
// precisely because GL needs PRIMITIVES_GENERATED without a capture, so
|
||||
// its semantics are exact by definition - what remains to prove is that
|
||||
// the DRIVER's implementation is not silent in the same way its stream
|
||||
// query is;
|
||||
// - a VK_QUERY_TYPE_PIPELINE_STATISTICS slot counting CLIPPING_INVOCATIONS
|
||||
// (when the device has pipelineStatisticsQuery): one invocation of the
|
||||
// primitive clipping stage per primitive reaching it - GL's
|
||||
// CLIPPING_INPUT_PRIMITIVES - which sits AFTER every vertex processing
|
||||
// stage (post-tess, post-GS) and, per spec, BEFORE rasterizer discard, so
|
||||
// for an XFB-inactive draw it is definitionally the number
|
||||
// PRIMITIVES_GENERATED must answer. (A geometry stage's non-zero vertex
|
||||
// streams never reach clipping, but non-indexed GL_PRIMITIVES_GENERATED
|
||||
// counts stream 0 alone, so the sets still agree. The stage's OUTPUT
|
||||
// count - CLIPPING_PRIMITIVES - would not: clipping drops and splits.)
|
||||
//
|
||||
// THE CONTROL DISCIPLINE (DriverBugProbes.h): the substitute slots are the
|
||||
// probe's controls, and the DISCARD dimension is measured separately because
|
||||
// it is a real fault line, not paranoia: Mesa llvmpipe short-circuits its
|
||||
// clipping statistics under rasterizer discard (reading 0 there while counting
|
||||
// the identical undiscarded draw exactly) while its dedicated
|
||||
// primitives-generated query counts both - measured 2026-08, and the reason
|
||||
// the verdict ranks the dedicated query first. A substitute qualifies only by
|
||||
// answering the exact expected count on every shape it is required to cover;
|
||||
// a device where no substitute qualifies even for the plain shape gets none
|
||||
// (the honest verdict is the current behaviour); anything that fits neither
|
||||
// the defect nor health is INCONCLUSIVE and must never arm anything. The
|
||||
// expected counts are exact (1 triangle per shape), not merely nonzero, so a
|
||||
// driver that half-counts cannot arm a half-right repair.
|
||||
|
||||
// What one drawn shape of the probe measured.
|
||||
struct PrimitivesGeneratedNoXfbShapeMeasurement {
|
||||
// The shape's draw was recorded and its query slots were read back.
|
||||
Bool drawn = false;
|
||||
// Primitives the draw is defined to emit (1 for every shape).
|
||||
Uint64 expectedPrimitives = 0;
|
||||
// The stream-query slot's primitivesNeeded answer - what the renderer's
|
||||
// GL_PRIMITIVES_GENERATED path would have returned.
|
||||
Uint64 streamGenerated = 0;
|
||||
// Whether the dedicated primitives-generated slot ran (it needs the
|
||||
// extension with both feature bits, see above).
|
||||
Bool primitivesGeneratedExtMeasured = false;
|
||||
// Its answer for the same draw.
|
||||
Uint64 primitivesGeneratedExt = 0;
|
||||
// Whether the statistics slot ran (it needs pipelineStatisticsQuery).
|
||||
Bool statisticsMeasured = false;
|
||||
// The clipping-stage invocation count for the same draw.
|
||||
Uint64 statisticsClippingInput = 0;
|
||||
};
|
||||
|
||||
struct PrimitivesGeneratedNoXfbMeasurement {
|
||||
// The probe submitted and read back at least the two triangle shapes.
|
||||
// False when any setup step failed; failureReason then names the step.
|
||||
Bool ran = false;
|
||||
// The probe's bounded fence wait expired with the submission possibly
|
||||
// still executing. The probe then deliberately LEAKED every child object
|
||||
// it created (no vkDeviceWaitIdle, no destroys - a hung GPU must not hang
|
||||
// the caller), so a caller that owns the device MUST NOT destroy or
|
||||
// idle-wait it either: vkDestroyDevice with live children and in-flight
|
||||
// work is the exact hang the bound exists to prevent. The POST leaks its
|
||||
// throwaway device on this flag, mirroring its sibling probes.
|
||||
Bool fenceWaitTimedOut = false;
|
||||
String failureReason;
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement trianglesPlain;
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement trianglesDiscard;
|
||||
// drawn = false when the device has no tessellationShader.
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement patchesDiscard;
|
||||
};
|
||||
|
||||
// The device-level entry points the probe records with. Supplied by the caller
|
||||
// because the two callers resolve them differently: the renderer passes its
|
||||
// statically linked symbols (and its vkGetDeviceProcAddr-resolved EXT
|
||||
// pointers), the driver POST passes vkGetInstanceProcAddr trampolines.
|
||||
struct PrimitivesGeneratedNoXfbProbeFns {
|
||||
PFN_vkCreateCommandPool vkCreateCommandPool = nullptr;
|
||||
PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr;
|
||||
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr;
|
||||
PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr;
|
||||
PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr;
|
||||
PFN_vkCreateQueryPool vkCreateQueryPool = nullptr;
|
||||
PFN_vkDestroyQueryPool vkDestroyQueryPool = nullptr;
|
||||
PFN_vkCmdResetQueryPool vkCmdResetQueryPool = nullptr;
|
||||
PFN_vkCmdBeginQuery vkCmdBeginQuery = nullptr;
|
||||
PFN_vkCmdEndQuery vkCmdEndQuery = nullptr;
|
||||
PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT = nullptr;
|
||||
PFN_vkCmdEndQueryIndexedEXT vkCmdEndQueryIndexedEXT = nullptr;
|
||||
PFN_vkCreateRenderPass vkCreateRenderPass = nullptr;
|
||||
PFN_vkDestroyRenderPass vkDestroyRenderPass = nullptr;
|
||||
PFN_vkCreateFramebuffer vkCreateFramebuffer = nullptr;
|
||||
PFN_vkDestroyFramebuffer vkDestroyFramebuffer = nullptr;
|
||||
PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass = nullptr;
|
||||
PFN_vkCmdEndRenderPass vkCmdEndRenderPass = nullptr;
|
||||
PFN_vkCreateShaderModule vkCreateShaderModule = nullptr;
|
||||
PFN_vkDestroyShaderModule vkDestroyShaderModule = nullptr;
|
||||
PFN_vkCreatePipelineLayout vkCreatePipelineLayout = nullptr;
|
||||
PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout = nullptr;
|
||||
PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines = nullptr;
|
||||
PFN_vkDestroyPipeline vkDestroyPipeline = nullptr;
|
||||
PFN_vkCmdBindPipeline vkCmdBindPipeline = nullptr;
|
||||
PFN_vkCmdDraw vkCmdDraw = nullptr;
|
||||
PFN_vkCreateFence vkCreateFence = nullptr;
|
||||
PFN_vkDestroyFence vkDestroyFence = nullptr;
|
||||
PFN_vkQueueSubmit vkQueueSubmit = nullptr;
|
||||
PFN_vkWaitForFences vkWaitForFences = nullptr;
|
||||
PFN_vkGetQueryPoolResults vkGetQueryPoolResults = nullptr;
|
||||
PFN_vkDeviceWaitIdle vkDeviceWaitIdle = nullptr;
|
||||
};
|
||||
|
||||
struct PrimitivesGeneratedNoXfbProbeContext {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
Uint32 queueFamilyIndex = 0;
|
||||
// The device was created with VK_EXT_transform_feedback, its
|
||||
// transformFeedback feature, and advertises transformFeedbackQueries.
|
||||
// Without this the probe has no subject and reports "did not run".
|
||||
Bool transformFeedbackQueriesUsable = false;
|
||||
// The device was created with VK_EXT_primitives_generated_query and BOTH
|
||||
// its primitivesGeneratedQuery and ...WithRasterizerDiscard features;
|
||||
// gates the dedicated-query control slots.
|
||||
Bool primitivesGeneratedQueryUsable = false;
|
||||
// The device was created with the pipelineStatisticsQuery feature; gates
|
||||
// the statistics control slots. A probe with no control at all can still
|
||||
// DETECT, but never qualifies a substitute.
|
||||
Bool pipelineStatisticsEnabled = false;
|
||||
// The device was created with the tessellationShader feature; gates the
|
||||
// PATCHES shape.
|
||||
Bool tessellationEnabled = false;
|
||||
PrimitivesGeneratedNoXfbProbeFns fns;
|
||||
};
|
||||
|
||||
// Records, submits and reads back the probe. Synchronous: waits on its own
|
||||
// fence (bounded; on timeout it deliberately leaks its device objects rather
|
||||
// than idle-wait a possibly hung GPU, mirroring the POST timestamp probe) and
|
||||
// destroys everything it created. Never touches MG_State or renderer state -
|
||||
// the caller only lends it a device and an otherwise idle queue.
|
||||
PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe(
|
||||
const PrimitivesGeneratedNoXfbProbeContext& context);
|
||||
|
||||
// The verdict vocabulary. Pure function of the measurement, split from the
|
||||
// Vulkan plumbing so a unit test can pin every mapping with synthetic numbers.
|
||||
enum class PrimitivesGeneratedNoXfbVerdict : Uint8 {
|
||||
// The probe did not run, or answered something that is neither healthy nor
|
||||
// the defect (a half-count, a nonzero-but-wrong stream answer). Must never
|
||||
// arm the reroute and must never be reported as the bug.
|
||||
Inconclusive,
|
||||
// Every drawn shape's stream query answered its exact expected count: the
|
||||
// driver counts XFB-inactive draws and the existing path is correct.
|
||||
StreamCounts,
|
||||
// The defect is present (a drawn shape's stream query answered exactly 0)
|
||||
// and the dedicated primitives-generated query answered every drawn shape
|
||||
// exactly, the rasterizer-discard shapes included: the substitution is
|
||||
// proven whole through the query Vulkan defines for exactly this GL
|
||||
// target.
|
||||
PrimitivesGeneratedExtSubstitute,
|
||||
// The defect is present, the dedicated query did not qualify (absent, or
|
||||
// silent like the stream query), and the statistics control answered EVERY
|
||||
// drawn shape exactly - discard shapes included: the substitution is
|
||||
// proven whole through clipping statistics.
|
||||
StatisticsSubstitute,
|
||||
// The defect is present and the statistics control is exact on the PLAIN
|
||||
// shape but not on every drawn shape (llvmpipe's discard short-circuit
|
||||
// does this to its statistics - its dedicated query is what rescues it to
|
||||
// the verdict above). This verdict additionally GUARANTEES domination:
|
||||
// every shape the statistics missed measured exactly 0 through the stream
|
||||
// query too, so rerouting is never worse per draw - it repairs every
|
||||
// shape the statistics answer exactly and leaves the rest at the 0 they
|
||||
// already read. A measurement where the stream was EXACT on a shape the
|
||||
// statistics missed does not qualify (rerouting would downgrade that
|
||||
// shape) and falls to Unfixable instead. The shapes the substitute
|
||||
// misses - the CTS's discarded shapes among them wherever they are the
|
||||
// missed ones - stay broken, and the report must say which.
|
||||
StatisticsSubstitutePlainOnly,
|
||||
// The defect is present and no substitute qualifies: none is exact
|
||||
// everywhere, and the plain-only fallback either misses the plain shape
|
||||
// or fails the domination rule above. The honest verdict is the current
|
||||
// behaviour.
|
||||
Unfixable,
|
||||
};
|
||||
|
||||
PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict(
|
||||
const PrimitivesGeneratedNoXfbMeasurement& measurement);
|
||||
|
||||
// Which query pool the renderer routes GL_PRIMITIVES_GENERATED accumulation
|
||||
// for XFB-inactive draws through.
|
||||
enum class PrimGenRerouteKind : Uint8 {
|
||||
None,
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (needs the extension with both
|
||||
// feature bits - see the context flag).
|
||||
PrimitivesGeneratedExt,
|
||||
// VK_QUERY_TYPE_PIPELINE_STATISTICS over clipping invocations (needs
|
||||
// pipelineStatisticsQuery).
|
||||
ClippingStatistics,
|
||||
};
|
||||
|
||||
// The arming decision. Pure, so the override mapping is unit-pinnable:
|
||||
// - ForceOff never reroutes;
|
||||
// - ForceOn bypasses the verdict but never the structural checks: it takes
|
||||
// the dedicated query where the device can host it, the statistics pool
|
||||
// where only that exists, and nothing where neither does;
|
||||
// - Auto follows the verdict: the dedicated query on
|
||||
// PrimitivesGeneratedExtSubstitute, the statistics pool on
|
||||
// StatisticsSubstitute and StatisticsSubstitutePlainOnly (each already
|
||||
// implies its feature-backed control), and nothing otherwise.
|
||||
PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting,
|
||||
PrimitivesGeneratedNoXfbVerdict verdict,
|
||||
Bool primitivesGeneratedQueryUsable,
|
||||
Bool pipelineStatisticsEnabled);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -0,0 +1,24 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc
|
||||
// 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
|
||||
//
|
||||
// Tessellation control stage of the PATCHES variant of the
|
||||
// primitives-generated-without-transform-feedback probe. Every level is 1, so with
|
||||
// the evaluation stage's triangles domain the tessellator emits exactly one
|
||||
// triangle per patch - the expected count the probe checks the queries against.
|
||||
#version 450
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese
|
||||
// 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
|
||||
//
|
||||
// Tessellation evaluation stage of the PATCHES variant of the
|
||||
// primitives-generated-without-transform-feedback probe. Triangles domain: with the
|
||||
// control stage's all-1 levels the tessellator emits exactly one triangle per
|
||||
// patch. Like the vertex stage, it deliberately carries no Xfb execution mode.
|
||||
#version 450
|
||||
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert
|
||||
// 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
|
||||
//
|
||||
// Vertex stage of the primitives-generated-without-transform-feedback probe
|
||||
// (PrimitivesGeneratedNoXfbProbe.cpp), used by both triangle shapes (with and
|
||||
// without rasterizer discard) and as the tessellation shapes' vertex stage.
|
||||
// Deliberately carries NO Xfb execution mode: the probe's whole subject is what
|
||||
// the transform-feedback stream query answers for a pipeline that captures
|
||||
// nothing. Positions are distinct (a full-viewport triangle per three vertices)
|
||||
// so no driver can excuse the primitive as degenerate before it reaches
|
||||
// primitive assembly.
|
||||
#version 450
|
||||
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(corners[gl_VertexIndex % 3], 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// Generated from PrimitivesGeneratedNoXfbProbe.{vert,tesc,tese} with:
|
||||
// glslangValidator --target-env vulkan1.1 -V PrimitivesGeneratedNoXfbProbe.<stage>
|
||||
// (SPIR-V words dumped little-endian, six per line.)
|
||||
//
|
||||
// Regenerate whenever a probe shader changes; nothing else in the probe depends on
|
||||
// the exact binary. None of the modules carries an Xfb execution mode - that is the
|
||||
// probe's subject, see PrimitivesGeneratedNoXfbProbe.cpp.
|
||||
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeVertSpv[] = {
|
||||
0x07230203, 0x00010300, 0x0008000b, 0x0000002a, 0x00000000, 0x00020011,
|
||||
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000000,
|
||||
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x0000001a, 0x00030003,
|
||||
0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000,
|
||||
0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000,
|
||||
0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69,
|
||||
0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953,
|
||||
0x00000000, 0x00070006, 0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c,
|
||||
0x61747369, 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, 0x435f6c67,
|
||||
0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000,
|
||||
0x00060005, 0x0000001a, 0x565f6c67, 0x65747265, 0x646e4978, 0x00007865,
|
||||
0x00050005, 0x0000001f, 0x65646e69, 0x6c626178, 0x00000065, 0x00030047,
|
||||
0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b,
|
||||
0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001,
|
||||
0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048,
|
||||
0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x0000001a,
|
||||
0x0000000b, 0x0000002a, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
|
||||
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
|
||||
0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000,
|
||||
0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a,
|
||||
0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006,
|
||||
0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b,
|
||||
0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e,
|
||||
0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000,
|
||||
0x00040017, 0x00000010, 0x00000006, 0x00000002, 0x0004002b, 0x00000008,
|
||||
0x00000011, 0x00000003, 0x0004001c, 0x00000012, 0x00000010, 0x00000011,
|
||||
0x0004002b, 0x00000006, 0x00000013, 0xbf800000, 0x0005002c, 0x00000010,
|
||||
0x00000014, 0x00000013, 0x00000013, 0x0004002b, 0x00000006, 0x00000015,
|
||||
0x40400000, 0x0005002c, 0x00000010, 0x00000016, 0x00000015, 0x00000013,
|
||||
0x0005002c, 0x00000010, 0x00000017, 0x00000013, 0x00000015, 0x0006002c,
|
||||
0x00000012, 0x00000018, 0x00000014, 0x00000016, 0x00000017, 0x00040020,
|
||||
0x00000019, 0x00000001, 0x0000000e, 0x0004003b, 0x00000019, 0x0000001a,
|
||||
0x00000001, 0x0004002b, 0x0000000e, 0x0000001c, 0x00000003, 0x00040020,
|
||||
0x0000001e, 0x00000007, 0x00000012, 0x00040020, 0x00000020, 0x00000007,
|
||||
0x00000010, 0x0004002b, 0x00000006, 0x00000023, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x00000024, 0x3f800000, 0x00040020, 0x00000028, 0x00000003,
|
||||
0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x0004003b, 0x0000001e, 0x0000001f, 0x00000007,
|
||||
0x0004003d, 0x0000000e, 0x0000001b, 0x0000001a, 0x0005008b, 0x0000000e,
|
||||
0x0000001d, 0x0000001b, 0x0000001c, 0x0003003e, 0x0000001f, 0x00000018,
|
||||
0x00050041, 0x00000020, 0x00000021, 0x0000001f, 0x0000001d, 0x0004003d,
|
||||
0x00000010, 0x00000022, 0x00000021, 0x00050051, 0x00000006, 0x00000025,
|
||||
0x00000022, 0x00000000, 0x00050051, 0x00000006, 0x00000026, 0x00000022,
|
||||
0x00000001, 0x00070050, 0x00000007, 0x00000027, 0x00000025, 0x00000026,
|
||||
0x00000023, 0x00000024, 0x00050041, 0x00000028, 0x00000029, 0x0000000d,
|
||||
0x0000000f, 0x0003003e, 0x00000029, 0x00000027, 0x000100fd, 0x00010038,
|
||||
};
|
||||
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeVertSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv[0]);
|
||||
|
||||
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTescSpv[] = {
|
||||
0x07230203, 0x00010300, 0x0008000b, 0x0000001d, 0x00000000, 0x00020011,
|
||||
0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000001,
|
||||
0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001a, 0x00040010,
|
||||
0x00000004, 0x0000001a, 0x00000001, 0x00030003, 0x00000002, 0x000001c2,
|
||||
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00070005, 0x0000000b,
|
||||
0x545f6c67, 0x4c737365, 0x6c657665, 0x6574754f, 0x00000072, 0x00070005,
|
||||
0x0000001a, 0x545f6c67, 0x4c737365, 0x6c657665, 0x656e6e49, 0x00000072,
|
||||
0x00040047, 0x0000000b, 0x0000000b, 0x0000000b, 0x00030047, 0x0000000b,
|
||||
0x0000000f, 0x00040047, 0x0000001a, 0x0000000b, 0x0000000c, 0x00030047,
|
||||
0x0000001a, 0x0000000f, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
|
||||
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040015, 0x00000007,
|
||||
0x00000020, 0x00000000, 0x0004002b, 0x00000007, 0x00000008, 0x00000004,
|
||||
0x0004001c, 0x00000009, 0x00000006, 0x00000008, 0x00040020, 0x0000000a,
|
||||
0x00000003, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000003,
|
||||
0x00040015, 0x0000000c, 0x00000020, 0x00000001, 0x0004002b, 0x0000000c,
|
||||
0x0000000d, 0x00000000, 0x0004002b, 0x00000006, 0x0000000e, 0x3f800000,
|
||||
0x00040020, 0x0000000f, 0x00000003, 0x00000006, 0x0004002b, 0x0000000c,
|
||||
0x00000011, 0x00000001, 0x0004002b, 0x0000000c, 0x00000013, 0x00000002,
|
||||
0x0004002b, 0x0000000c, 0x00000015, 0x00000003, 0x0004002b, 0x00000007,
|
||||
0x00000017, 0x00000002, 0x0004001c, 0x00000018, 0x00000006, 0x00000017,
|
||||
0x00040020, 0x00000019, 0x00000003, 0x00000018, 0x0004003b, 0x00000019,
|
||||
0x0000001a, 0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||
0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x0000000f, 0x00000010,
|
||||
0x0000000b, 0x0000000d, 0x0003003e, 0x00000010, 0x0000000e, 0x00050041,
|
||||
0x0000000f, 0x00000012, 0x0000000b, 0x00000011, 0x0003003e, 0x00000012,
|
||||
0x0000000e, 0x00050041, 0x0000000f, 0x00000014, 0x0000000b, 0x00000013,
|
||||
0x0003003e, 0x00000014, 0x0000000e, 0x00050041, 0x0000000f, 0x00000016,
|
||||
0x0000000b, 0x00000015, 0x0003003e, 0x00000016, 0x0000000e, 0x00050041,
|
||||
0x0000000f, 0x0000001b, 0x0000001a, 0x0000000d, 0x0003003e, 0x0000001b,
|
||||
0x0000000e, 0x00050041, 0x0000000f, 0x0000001c, 0x0000001a, 0x00000011,
|
||||
0x0003003e, 0x0000001c, 0x0000000e, 0x000100fd, 0x00010038,
|
||||
};
|
||||
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTescSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv[0]);
|
||||
|
||||
inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTeseSpv[] = {
|
||||
0x07230203, 0x00010300, 0x0008000b, 0x00000021, 0x00000000, 0x00020011,
|
||||
0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
|
||||
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000002,
|
||||
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000012, 0x00030010,
|
||||
0x00000004, 0x00000016, 0x00030010, 0x00000004, 0x00000001, 0x00030010,
|
||||
0x00000004, 0x00000004, 0x00030003, 0x00000002, 0x000001c2, 0x00040005,
|
||||
0x00000004, 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67,
|
||||
0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000,
|
||||
0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, 0x0000000b, 0x00000001,
|
||||
0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000000b,
|
||||
0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006,
|
||||
0x0000000b, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e,
|
||||
0x00030005, 0x0000000d, 0x00000000, 0x00060005, 0x00000012, 0x545f6c67,
|
||||
0x43737365, 0x64726f6f, 0x00000000, 0x00030047, 0x0000000b, 0x00000002,
|
||||
0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048,
|
||||
0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b,
|
||||
0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, 0x00000003,
|
||||
0x0000000b, 0x00000004, 0x00040047, 0x00000012, 0x0000000b, 0x0000000d,
|
||||
0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
|
||||
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004,
|
||||
0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008,
|
||||
0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009,
|
||||
0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a,
|
||||
0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c,
|
||||
0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001,
|
||||
0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040017, 0x00000010,
|
||||
0x00000006, 0x00000003, 0x00040020, 0x00000011, 0x00000001, 0x00000010,
|
||||
0x0004003b, 0x00000011, 0x00000012, 0x00000001, 0x00040017, 0x00000013,
|
||||
0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x00000016, 0x40000000,
|
||||
0x0004002b, 0x00000006, 0x00000018, 0x3f800000, 0x0004002b, 0x00000006,
|
||||
0x0000001b, 0x00000000, 0x00040020, 0x0000001f, 0x00000003, 0x00000007,
|
||||
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8,
|
||||
0x00000005, 0x0004003d, 0x00000010, 0x00000014, 0x00000012, 0x0007004f,
|
||||
0x00000013, 0x00000015, 0x00000014, 0x00000014, 0x00000000, 0x00000001,
|
||||
0x0005008e, 0x00000013, 0x00000017, 0x00000015, 0x00000016, 0x00050050,
|
||||
0x00000013, 0x00000019, 0x00000018, 0x00000018, 0x00050083, 0x00000013,
|
||||
0x0000001a, 0x00000017, 0x00000019, 0x00050051, 0x00000006, 0x0000001c,
|
||||
0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d, 0x0000001a,
|
||||
0x00000001, 0x00070050, 0x00000007, 0x0000001e, 0x0000001c, 0x0000001d,
|
||||
0x0000001b, 0x00000018, 0x00050041, 0x0000001f, 0x00000020, 0x0000000d,
|
||||
0x0000000f, 0x0003003e, 0x00000020, 0x0000001e, 0x000100fd, 0x00010038,
|
||||
};
|
||||
inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv[0]);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -154,6 +154,20 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// demoted module is the one that works everywhere, so it is what a standalone compile
|
||||
// (an internal shader object, a unit test) gets.
|
||||
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
|
||||
// Whether the tessellation / geometry gl_PointSize demotion is ARMED for this env -
|
||||
// i.e. the backend declared it cannot host the capability. Deliberately requiring a
|
||||
// backend, opposite in shape to ConsumesFloat64Natively's fallback but for the same
|
||||
// conservatism: the fp64 demotion is the module that works everywhere, while this
|
||||
// one rewrites interfaces and capture names, so the no-backend answer (standalone
|
||||
// compiles, unit tests) is the untouched module. Like nativeFloat64, each bit is L1
|
||||
// key material of its own (SpirvTranslationKeyInputs), never part of the frontend
|
||||
// fingerprint: glslang produces the same thing either way.
|
||||
Bool DemotesTessellationPointSize() const {
|
||||
return HasBackend() && !params.SupportsTessellationPointSize;
|
||||
}
|
||||
Bool DemotesGeometryPointSize() const {
|
||||
return HasBackend() && !params.SupportsGeometryPointSize;
|
||||
}
|
||||
// Matches the historical rule exactly: with no active backend every extension counts
|
||||
// as advertised, because the frontend then has nothing to gate against.
|
||||
Bool IsExtensionAdvertised(GLExtension extension) const {
|
||||
|
||||
@@ -45,12 +45,14 @@
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripIoBlockLocationsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||
#include "SpirvPasses/DemotePointSizePass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
@@ -758,6 +760,383 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresTransformFeedback"),
|
||||
spirv.data(), spirv.size());
|
||||
if (!context) {
|
||||
// Unparseable is not a capture verdict; say no, which makes the caller decline
|
||||
// the span rather than issue transform-feedback commands against it.
|
||||
return false;
|
||||
}
|
||||
// The exact question VUID-vkCmdBeginTransformFeedbackEXT-None-04128 asks of the
|
||||
// bound pipeline's last pre-rasterization stage: was it declared with the Xfb
|
||||
// execution mode. Reading the execution modes rather than the TransformFeedback
|
||||
// capability because the capability can legally be declared by a module that has
|
||||
// no Xfb entry point, and the VUID is about the mode.
|
||||
for (const spvtools::opt::Instruction& mode : context->module()->execution_modes()) {
|
||||
if (mode.NumInOperands() >= 2 &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
|
||||
spv::ExecutionMode::Xfb) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1,
|
||||
MakeSpirvMessageConsumer("ModuleDeclaresTessellationOrGeometryPointSize"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
// Unparseable is not a verdict about point size. Say no, so the caller keeps
|
||||
// building the program: the module is already broken for other reasons and
|
||||
// the diagnostics that own that failure are better placed than this one.
|
||||
return false;
|
||||
}
|
||||
// The CAPABILITY, not the BuiltIn decoration, because the capability is exactly
|
||||
// what the feature gates: a module may declare gl_PerVertex with a PointSize
|
||||
// member and never access it, and glslang then emits no capability
|
||||
// (GlslangToSpv defers it to actual use) - such a module is legal without the
|
||||
// feature and must not be declined.
|
||||
for (const spvtools::opt::Instruction& capability : context->capabilities()) {
|
||||
if (capability.NumInOperands() < 1) continue;
|
||||
const auto declared = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (declared == spv::Capability::TessellationPointSize ||
|
||||
declared == spv::Capability::GeometryPointSize) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
namespace opt_analysis = spvtools::opt::analysis;
|
||||
|
||||
// Locations one value of `type` consumes (GL 4.6 core 11.1.2.1). Unknown
|
||||
// shapes OVERESTIMATE (4) rather than fail: this feeds the free-location
|
||||
// choice for the demoted point-size carrier, where an overestimate wastes a
|
||||
// couple of slots and an underestimate aliases a live varying.
|
||||
Uint32 ConservativeLocationSpan(const opt_analysis::Type* type) {
|
||||
constexpr Uint32 kUnknownSpan = 4;
|
||||
if (type == nullptr) return kUnknownSpan;
|
||||
if (type->AsFloat() != nullptr || type->AsInteger() != nullptr ||
|
||||
type->AsBool() != nullptr) {
|
||||
return 1u;
|
||||
}
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
// 64-bit INTEGER elements count exactly like 64-bit floats:
|
||||
// ARB_gpu_shader_int64 extends 11.1.2.1's double-precision rule
|
||||
// verbatim to i64/u64, and DirectVulkan advertises that extension
|
||||
// unconditionally - so answering "one location" for an i64vec4 would
|
||||
// place the carrier on the SECOND location that varying already owns,
|
||||
// which is the underestimate this function's header forbids.
|
||||
const auto* element = vector->element_type();
|
||||
const auto* elementFloat = element->AsFloat();
|
||||
const auto* elementInteger = element->AsInteger();
|
||||
const Bool is64Bit = (elementFloat != nullptr && elementFloat->width() == 64) ||
|
||||
(elementInteger != nullptr && elementInteger->width() == 64);
|
||||
return (is64Bit && vector->element_count() > 2) ? 2u : 1u;
|
||||
}
|
||||
if (const auto* matrix = type->AsMatrix()) {
|
||||
return ConservativeLocationSpan(matrix->element_type()) * matrix->element_count();
|
||||
}
|
||||
if (const auto* array = type->AsArray()) {
|
||||
const auto& lengthWords = array->length_info().words;
|
||||
if (lengthWords.size() != 2 ||
|
||||
lengthWords[0] !=
|
||||
static_cast<Uint32>(opt_analysis::Array::LengthInfo::kConstant)) {
|
||||
return kUnknownSpan;
|
||||
}
|
||||
return ConservativeLocationSpan(array->element_type()) * std::max(lengthWords[1], 1u);
|
||||
}
|
||||
if (const auto* strct = type->AsStruct()) {
|
||||
Uint32 sum = 0;
|
||||
for (const auto* member : strct->element_types()) {
|
||||
sum += ConservativeLocationSpan(member);
|
||||
}
|
||||
return std::max(sum, 1u);
|
||||
}
|
||||
return kUnknownSpan;
|
||||
}
|
||||
|
||||
// One BuildModule per module answers all three questions the program-scoped
|
||||
// demotion driver asks: which point-size capability the module declares, and
|
||||
// one past the highest Input/Output location slot it consumes (so the carrier
|
||||
// can be placed beyond every varying of every stage).
|
||||
struct PointSizeModuleProbe {
|
||||
Bool parsed = false;
|
||||
Bool declaresTessellationPointSize = false;
|
||||
Bool declaresGeometryPointSize = false;
|
||||
Uint32 locationSlotEnd = 0;
|
||||
};
|
||||
|
||||
PointSizeModuleProbe ProbePointSizeModule(const Vector<Uint32>& spirv) {
|
||||
PointSizeModuleProbe probe;
|
||||
if (spirv.empty()) {
|
||||
probe.parsed = true; // an absent stage constrains nothing
|
||||
return probe;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbePointSizeModule"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) return probe;
|
||||
probe.parsed = true;
|
||||
|
||||
for (const spvtools::opt::Instruction& capability : context->capabilities()) {
|
||||
if (capability.NumInOperands() < 1) continue;
|
||||
const auto declared =
|
||||
static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (declared == spv::Capability::TessellationPointSize) {
|
||||
probe.declaresTessellationPointSize = true;
|
||||
} else if (declared == spv::Capability::GeometryPointSize) {
|
||||
probe.declaresGeometryPointSize = true;
|
||||
}
|
||||
}
|
||||
|
||||
spv::ExecutionModel model = spv::ExecutionModel::Max;
|
||||
for (spvtools::opt::Instruction& entryPoint : context->module()->entry_points()) {
|
||||
model = static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0));
|
||||
break;
|
||||
}
|
||||
// Per-vertex interfaces are arrayed one level deeper than the locations
|
||||
// they consume; peel that level, but never off a per-patch output.
|
||||
const Bool peelInputs = model == spv::ExecutionModel::TessellationControl ||
|
||||
model == spv::ExecutionModel::TessellationEvaluation ||
|
||||
model == spv::ExecutionModel::Geometry;
|
||||
const Bool peelOutputs = model == spv::ExecutionModel::TessellationControl;
|
||||
|
||||
std::unordered_set<Uint32> patchDecorated;
|
||||
for (spvtools::opt::Instruction& annotation : context->annotations()) {
|
||||
if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 2 &&
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::Patch) {
|
||||
patchDecorated.insert(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
}
|
||||
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
auto* typeMgr = context->get_type_mgr();
|
||||
for (spvtools::opt::Instruction& annotation : context->annotations()) {
|
||||
if (annotation.opcode() == spv::Op::OpDecorate && annotation.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::Location) {
|
||||
const Uint32 location = annotation.GetSingleWordInOperand(2);
|
||||
Uint32 span = 1;
|
||||
spvtools::opt::Instruction* var =
|
||||
defUse->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
||||
const auto storage =
|
||||
static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0));
|
||||
// Two location namespaces are NOT varying slots and must not
|
||||
// shrink the carrier budget: vertex-stage inputs (attribute
|
||||
// locations) and fragment-stage outputs (draw buffers).
|
||||
if ((model == spv::ExecutionModel::Vertex &&
|
||||
storage == spv::StorageClass::Input) ||
|
||||
(model == spv::ExecutionModel::Fragment &&
|
||||
storage == spv::StorageClass::Output)) {
|
||||
continue;
|
||||
}
|
||||
spvtools::opt::Instruction* pointerType = defUse->GetDef(var->type_id());
|
||||
if (pointerType != nullptr &&
|
||||
pointerType->opcode() == spv::Op::OpTypePointer) {
|
||||
const opt_analysis::Type* pointee =
|
||||
typeMgr->GetType(pointerType->GetSingleWordInOperand(1));
|
||||
const Bool peel =
|
||||
((storage == spv::StorageClass::Input && peelInputs) ||
|
||||
(storage == spv::StorageClass::Output && peelOutputs)) &&
|
||||
patchDecorated.count(var->result_id()) == 0;
|
||||
if (peel && pointee != nullptr && pointee->AsArray() != nullptr) {
|
||||
pointee = pointee->AsArray()->element_type();
|
||||
}
|
||||
span = ConservativeLocationSpan(pointee);
|
||||
}
|
||||
}
|
||||
probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span);
|
||||
} else if (annotation.opcode() == spv::Op::OpMemberDecorate &&
|
||||
annotation.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::Location) {
|
||||
const Uint32 member = annotation.GetSingleWordInOperand(1);
|
||||
const Uint32 location = annotation.GetSingleWordInOperand(3);
|
||||
Uint32 span = 1;
|
||||
spvtools::opt::Instruction* structType =
|
||||
defUse->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (structType != nullptr && structType->opcode() == spv::Op::OpTypeStruct &&
|
||||
member < structType->NumInOperands()) {
|
||||
span = ConservativeLocationSpan(
|
||||
typeMgr->GetType(structType->GetSingleWordInOperand(member)));
|
||||
}
|
||||
probe.locationSlotEnd = std::max(probe.locationSlotEnd, location + span);
|
||||
}
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
// Interior boundary carrier names, spelled by the PRODUCING stage so both
|
||||
// sides of one boundary agree textually as well as by location. The capture
|
||||
// stage's output uses POINT_SIZE_CAPTURE_CARRIER_NAME instead. None of these
|
||||
// may embed the token "gl_PointSize" - see the constant's comment.
|
||||
const char* PointSizeBoundaryCarrierName(const GLenum producerStage) {
|
||||
switch (producerStage) {
|
||||
case GL_VERTEX_SHADER:
|
||||
return "mg_PointSizeIo0";
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
return "mg_PointSizeIo1";
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
return "mg_PointSizeIo2";
|
||||
default:
|
||||
return "mg_PointSizeIo0";
|
||||
}
|
||||
}
|
||||
|
||||
// Past this the carrier would sit above what a minimum-spec varying budget can
|
||||
// address; such a program keeps its honest decline instead.
|
||||
constexpr Uint32 kMaxDemotedPointSizeCarrierLocation = 30;
|
||||
} // namespace
|
||||
|
||||
Bool ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram(
|
||||
Vector<Vector<Uint32>>& modules, const Vector<GLenum>& shaderTypes,
|
||||
const Bool demoteTessellation, const Bool demoteGeometry,
|
||||
const Bool captureRequestsPointSize, PointSizeDemotionOutcome& outcome,
|
||||
const bool validateOutput, const bool enableSpirvValidation) {
|
||||
outcome = {};
|
||||
if (!demoteTessellation && !demoteGeometry) return true;
|
||||
|
||||
// The pre-rasterization chain, in pipeline order, as indices into `modules`.
|
||||
Int stageIndex[4] = {-1, -1, -1, -1}; // VS, TCS, TES, GS
|
||||
for (SizeT i = 0; i < shaderTypes.size() && i < modules.size(); ++i) {
|
||||
switch (shaderTypes[i]) {
|
||||
case GL_VERTEX_SHADER: stageIndex[0] = static_cast<Int>(i); break;
|
||||
case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast<Int>(i); break;
|
||||
case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast<Int>(i); break;
|
||||
case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast<Int>(i); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
if (stageIndex[1] < 0 && stageIndex[2] < 0 && stageIndex[3] < 0) return true;
|
||||
|
||||
// One probe per module: the capability facts arm the verdict, the location
|
||||
// scan places the carrier past every varying of every stage (the location is
|
||||
// shared program-wide, so it has to clear all of them at once).
|
||||
Bool anyTessellationUse = false;
|
||||
Bool anyGeometryUse = false;
|
||||
Uint32 carrierLocation = 0;
|
||||
for (const auto& module : modules) {
|
||||
const PointSizeModuleProbe probe = ProbePointSizeModule(module);
|
||||
if (!probe.parsed) {
|
||||
// Unparseable is not a verdict; the module is already broken for
|
||||
// other reasons and owns its own failure.
|
||||
return true;
|
||||
}
|
||||
anyTessellationUse |= probe.declaresTessellationPointSize;
|
||||
anyGeometryUse |= probe.declaresGeometryPointSize;
|
||||
carrierLocation = std::max(carrierLocation, probe.locationSlotEnd);
|
||||
}
|
||||
if (!((anyTessellationUse && demoteTessellation) ||
|
||||
(anyGeometryUse && demoteGeometry))) {
|
||||
return true;
|
||||
}
|
||||
if (carrierLocation > kMaxDemotedPointSizeCarrierLocation) {
|
||||
outcome.declineDetail = std::format(
|
||||
"the program's varyings already reach location {}, past the carrier budget",
|
||||
carrierLocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 13.3: capture reads the last capture-capable stage - geometry,
|
||||
// else evaluation, else the vertex stage (whose built-in needs no demotion).
|
||||
const Int captureStage = stageIndex[3] >= 0 ? 3 : (stageIndex[2] >= 0 ? 2 : -1);
|
||||
constexpr GLenum kStageEnum[4] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER};
|
||||
|
||||
// Back to front, so each stage's "I now read the carrier" report can force the
|
||||
// producing stage's output carrier into existence - Vulkan requires every
|
||||
// consumed input to be produced (VUID-RuntimeSpirv-OpEntryPoint-08743), and an
|
||||
// ES link may reject a statically read input with no producing output.
|
||||
Vector<Vector<Uint32>> rewritten(modules.size());
|
||||
Bool rewrote[4] = {false, false, false, false};
|
||||
Bool forceOutput[4] = {false, false, false, false};
|
||||
if (captureStage >= 0 && captureRequestsPointSize) {
|
||||
forceOutput[captureStage] = true;
|
||||
}
|
||||
for (Int stage = 3; stage >= 0; --stage) {
|
||||
const Int moduleIndex = stageIndex[stage];
|
||||
if (moduleIndex < 0) continue;
|
||||
Int producer = stage - 1;
|
||||
while (producer >= 0 && stageIndex[producer] < 0) --producer;
|
||||
|
||||
DemotePointSizeOptions options;
|
||||
options.location = carrierLocation;
|
||||
options.inputCarrierName = PointSizeBoundaryCarrierName(
|
||||
producer >= 0 ? kStageEnum[producer]
|
||||
// A separable program whose first present stage already
|
||||
// consumes the carrier: the producer lives in another
|
||||
// program. Name by the conventional producer of this
|
||||
// stage's boundary; matching across programs is by
|
||||
// location and is documented residue either way.
|
||||
: kStageEnum[stage > 0 ? stage - 1 : 0]);
|
||||
options.outputCarrierName = stage == captureStage
|
||||
? String(POINT_SIZE_CAPTURE_CARRIER_NAME)
|
||||
: String(PointSizeBoundaryCarrierName(kStageEnum[stage]));
|
||||
options.forceOutputCarrier = forceOutput[stage];
|
||||
|
||||
// A vertex stage with nothing downstream consuming the carrier needs no
|
||||
// mirror and stays byte-identical without an optimizer round trip.
|
||||
if (stage == 0 && !options.forceOutputCarrier) continue;
|
||||
|
||||
DemotePointSizeReport report;
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
DemotePointSizePass::CreateDemotePointSizePass(options, &report));
|
||||
if (!RunOptimizerChecked("DemoteTessellationGeometryPointSizeForProgram", optimizer,
|
||||
modules[moduleIndex], rewritten[moduleIndex],
|
||||
validateOutput, enableSpirvValidation)) {
|
||||
return false; // modules untouched: nothing was committed
|
||||
}
|
||||
if (report.declined) {
|
||||
outcome.declineDetail = Move(report.declineReason);
|
||||
return true; // byte-identical decline; the existing refusals stay armed
|
||||
}
|
||||
// AN EVALUATION STAGE WITH NO CONTROL STAGE THAT NOW READS A LOCATED
|
||||
// INPUT. GL lets the evaluation stage sit straight on the vertex stage,
|
||||
// and both backends stand a SYNTHESIZED pass-through control stage in
|
||||
// between - one that forwards gl_Position and nothing else. Their guard
|
||||
// for that is literally "does this module read a located input"
|
||||
// (ModuleReadsLocatedInput / ReflectPassthroughTessControlNeed), so the
|
||||
// carrier this pass just created would turn the very program the demotion
|
||||
// exists to rescue into a declined one, reported against a varying name
|
||||
// the application never wrote. Declining here keeps the modules
|
||||
// byte-identical and leaves the honest built-in refusal in charge; only
|
||||
// teaching the synthesized stage to forward the carrier could do better.
|
||||
if (stage == 2 && stageIndex[1] < 0 && report.createdInputCarrier) {
|
||||
outcome.declineDetail =
|
||||
"an evaluation stage reads gl_in point size with no control stage to "
|
||||
"carry it; the synthesized pass-through cannot forward the carrier";
|
||||
return true;
|
||||
}
|
||||
rewrote[stage] = true;
|
||||
if (report.createdInputCarrier && producer >= 0) {
|
||||
forceOutput[producer] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic commit: every stage rewritten together or none at all.
|
||||
for (Int stage = 0; stage < 4; ++stage) {
|
||||
if (!rewrote[stage]) continue;
|
||||
modules[stageIndex[stage]] = Move(rewritten[stageIndex[stage]]);
|
||||
}
|
||||
outcome.demoted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
@@ -1170,6 +1549,31 @@ namespace MobileGL {
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripIoBlockLocationsForEssl(const Vector<Uint32>& inputBinary,
|
||||
const bool stripInputBlocks,
|
||||
const bool stripOutputBlocks,
|
||||
bool& strippedAny,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
strippedAny = false;
|
||||
if (!stripInputBlocks && !stripOutputBlocks) return false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripIoBlockLocationsPass::CreateStripIoBlockLocationsPass(
|
||||
stripInputBlocks, stripOutputBlocks, &strippedAny));
|
||||
|
||||
// NOT VALIDATED, and that is the point of the pass rather than an oversight.
|
||||
// Vulkan SPIR-V requires a Location on every user-defined Input/Output variable
|
||||
// ([VUID-StandaloneSpirv-Location-04915]), so a module whose interface blocks
|
||||
// have deliberately lost theirs fails spirv-val by construction. It never
|
||||
// reaches a driver as SPIR-V: the caller runs this last in the DirectGLES chain
|
||||
// and hands the result straight to SPIRV-Cross, which needs no location to
|
||||
// print a block. Validating here would latch a failure on every affected
|
||||
// program and teach the counter to cry wolf.
|
||||
return RunOptimizerChecked("StripIoBlockLocationsForEssl", optimizer, inputBinary,
|
||||
outputBinary, false, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -154,6 +154,20 @@ namespace MobileGL {
|
||||
std::set<String>& renamedBlockNames,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Drops the Location (and Component) decoration from inter-stage interface
|
||||
// BLOCK variables, so SPIRV-Cross emits them unqualified and ES matches them
|
||||
// by block name plus member sequence. `stripInputBlocks` covers the blocks
|
||||
// this stage consumes and `stripOutputBlocks` the ones it produces - armed
|
||||
// separately because an interface whose other end is in a DIFFERENT program
|
||||
// must keep the location that matches it there. `strippedAny` reports whether
|
||||
// this stage actually had one. The Mali ES driver loses the payload of a
|
||||
// located block across any tessellation or geometry boundary; only for the
|
||||
// DirectGLES transpile path, and only when the driver POST says so. See
|
||||
// StripIoBlockLocationsPass.
|
||||
static bool StripIoBlockLocationsForEssl(const Vector<Uint32>& inputBinary,
|
||||
bool stripInputBlocks, bool stripOutputBlocks,
|
||||
bool& strippedAny, Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
@@ -532,6 +546,76 @@ namespace MobileGL {
|
||||
// check exists so that failure can be reported as the missing capability it is,
|
||||
// naming the shader, rather than as a driver info log nobody sees.
|
||||
static Bool ModuleDeclaresBufferTextureSampler(const Vector<Uint32>& spirv);
|
||||
// Does this module carry the Xfb execution mode - i.e. would a
|
||||
// vkCmdBeginTransformFeedbackEXT against a pipeline whose last pre-rasterization
|
||||
// stage is this module satisfy VUID-vkCmdBeginTransformFeedbackEXT-None-04128?
|
||||
// Asked of the FINAL bytes, so it answers for whatever the backend transform
|
||||
// chain actually produced rather than for what it was asked to produce.
|
||||
static Bool ModuleDeclaresTransformFeedback(const Vector<Uint32>& spirv);
|
||||
// Does this module declare TessellationPointSize or GeometryPointSize - i.e. does
|
||||
// it need VkPhysicalDeviceFeatures::shaderTessellationAndGeometryPointSize before
|
||||
// a pipeline built from it is legal usage (VUID-RuntimeSpirv-PointSize-06439)?
|
||||
// glslang emits either capability from any access to the PointSize built-in in a
|
||||
// tessellation or geometry stage, which desktop GL treats as an ordinary
|
||||
// per-vertex output, so a program that is perfectly legal in GL can need a Vulkan
|
||||
// feature the device does not have. Callers only ask when the feature is OFF, so
|
||||
// the module parse costs nothing on a device that has it.
|
||||
static Bool ModuleDeclaresTessellationOrGeometryPointSize(const Vector<Uint32>& spirv);
|
||||
|
||||
// ---- gl_PointSize demotion for devices without the capability above ----
|
||||
// The name of the demoted program's LAST capture-capable stage's point-size
|
||||
// carrier. It is the contract three parties meet at: the demotion pass names
|
||||
// the variable, DirectVulkan's XfbCaptureDecoratePass binds a "gl_PointSize"
|
||||
// capture to it instead of mirroring the (no longer accessed) built-in, and
|
||||
// DirectGLES respells the driver-side glTransformFeedbackVaryings request
|
||||
// with it. Deliberately NOT containing the substring "gl_PointSize":
|
||||
// DirectGLES's extension-request gate is a text search for that token over
|
||||
// the emitted ESSL, and a carrier name embedding it would re-arm the decline
|
||||
// this demotion exists to retire.
|
||||
static constexpr const char* POINT_SIZE_CAPTURE_CARRIER_NAME = "mg_PointSizeCapture";
|
||||
|
||||
// What the program-scoped demotion left behind. `demoted` false with an empty
|
||||
// detail means the program never needed it (no tessellation/geometry stage
|
||||
// accesses the built-in, or the device hosts it); false WITH a detail means a
|
||||
// module shape the pass cannot express - the modules are byte-identical and
|
||||
// the existing decline paths (Espryt's missing-extension compile failure,
|
||||
// Magma's pointSizeCapabilityUnsupported refusal) stay in charge of it.
|
||||
struct PointSizeDemotionOutcome {
|
||||
Bool demoted = false;
|
||||
String declineDetail;
|
||||
};
|
||||
|
||||
// Demotes gl_PointSize across a WHOLE program's pre-rasterization chain into
|
||||
// ordinary float varyings at one shared free location, so a device that
|
||||
// advertises neither ES tessellation/geometry_point_size extension nor
|
||||
// Vulkan's shaderTessellationAndGeometryPointSize can still run programs
|
||||
// whose tessellation/geometry stages merely CARRY the value (transform
|
||||
// feedback and gl_in[].gl_PointSize reads). Runs after
|
||||
// SanitizeAndOptimizeBinary, on the final shared modules both backends
|
||||
// consume, and is atomic per program: every stage is rewritten or none is,
|
||||
// because a consumer whose producer kept the built-in would read garbage.
|
||||
// `demoteTessellation` / `demoteGeometry` are the env verdicts (the device
|
||||
// LACKS that capability); the per-program half of the decision - whether any
|
||||
// module actually declares TessellationPointSize / GeometryPointSize - is
|
||||
// probed here. `captureRequestsPointSize` forces the capture-capable last
|
||||
// stage to declare its carrier even when it never writes the built-in, so a
|
||||
// by-name capture always has something to bind to. Returns false only when
|
||||
// the optimizer itself failed (modules untouched); a shape decline is
|
||||
// reported through `outcome` and also leaves the modules untouched. See
|
||||
// DemotePointSizePass for the per-module rewrite and its honest residue.
|
||||
//
|
||||
// Two declines are PROGRAM-shaped and therefore live here rather than in the
|
||||
// pass: a carrier that would land past the minimum-spec varying budget, and
|
||||
// an evaluation stage reading gl_in point size with NO control stage - the
|
||||
// synthesized pass-through control stage both backends stand in that gap
|
||||
// forwards gl_Position alone, so the input carrier would strand the value and
|
||||
// trip the backends' own "reads a located input" refusal against a name the
|
||||
// application never wrote.
|
||||
static Bool DemoteTessellationGeometryPointSizeForProgram(
|
||||
Vector<Vector<Uint32>>& modules, const Vector<GLenum>& shaderTypes,
|
||||
Bool demoteTessellation, Bool demoteGeometry, Bool captureRequestsPointSize,
|
||||
PointSizeDemotionOutcome& outcome, bool validateOutput = true,
|
||||
bool enableSpirvValidation = false);
|
||||
|
||||
// True when the module still declares a 64-bit float type. After
|
||||
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user