mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
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_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_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_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
|
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
|
||||||
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
||||||
|
|||||||
@@ -417,12 +417,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrace and validate
|
- name: Retrace and validate
|
||||||
env:
|
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_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_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_MAGMA_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_MAGMA_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_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
run: |
|
run: |
|
||||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||||
test -f "${apk_file}"
|
test -f "${apk_file}"
|
||||||
|
|||||||
@@ -265,16 +265,26 @@ jobs:
|
|||||||
# crash stack without burning a CI round on an in-workflow debugger.
|
# crash stack without burning a CI round on an in-workflow debugger.
|
||||||
env:
|
env:
|
||||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
|
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
|
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited
|
ulimit -c unlimited
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
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
|
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||||
ctest -V -L integration-gpu --no-tests=error
|
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
|
else
|
||||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
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
|
fi
|
||||||
|
|
||||||
- name: Upload core dumps
|
- name: Upload core dumps
|
||||||
@@ -644,9 +654,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||||
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
||||||
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||||
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1
|
||||||
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
export MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1
|
||||||
fi
|
fi
|
||||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
# 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
|
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.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/SplitArrayVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||||
|
|||||||
+66
-25
@@ -69,34 +69,34 @@ namespace MobileGL::MG_Config {
|
|||||||
struct FeaturesTable {
|
struct FeaturesTable {
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||||
Bool DisableTimerQuery = false;
|
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
|
// 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
|
// 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
|
// there, because the view's ES internalformat is normalized independently of the storage
|
||||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||||
// so that work can be done without editing the gate.
|
// 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.
|
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||||
// Disabled by default because validation is a diagnostics-only cost.
|
// Disabled by default because validation is a diagnostics-only cost.
|
||||||
Bool EnableSpirvValidation = false;
|
Bool EnableSpirvValidation = false;
|
||||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||||
Bool UseAngle = false;
|
Bool EsprytUseAngle = false;
|
||||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||||
String TraceAngleVariant;
|
String TraceAngleVariant;
|
||||||
#endif
|
#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.
|
// 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
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||||
// engages when this flag is set AND the device has no native subgroup support at
|
// 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,
|
// all - a device with real subgroup operations always uses them natively,
|
||||||
// whatever their width (the known iterationRP defect is patched by
|
// 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;
|
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
|
// 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
|
// 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
|
// 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
|
// 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
|
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||||
// verbatim.
|
// verbatim.
|
||||||
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||||
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||||
Bool IterationRPFixBarrier = false;
|
Bool MagmaIterationRPFixBarrier = false;
|
||||||
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
// 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
|
// 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
|
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||||
// to the raw driver builtin.
|
// 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
|
// 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
|
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||||
@@ -132,16 +132,26 @@ namespace MobileGL::MG_Config {
|
|||||||
Bool MagmaR11G11B10FFallback = false;
|
Bool MagmaR11G11B10FFallback = false;
|
||||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||||
Uint32 MagmaFramesInFlight = 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.
|
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||||
Bool AvoidSamplerMipmapMinFilter = false;
|
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||||
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
// 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
|
// 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
|
// 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
|
// 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
|
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
// 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_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
// 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
|
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||||
@@ -151,15 +161,29 @@ namespace MobileGL::MG_Config {
|
|||||||
Bool CoherentAsFlush = false;
|
Bool CoherentAsFlush = false;
|
||||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||||
Bool TraceSkipAutodestroy = false;
|
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
|
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||||
// (negative control / driver-bug escape hatch).
|
// (negative control / driver-bug escape hatch).
|
||||||
Bool DisableUboRing = false;
|
Bool EsprytDisableUboRing = false;
|
||||||
// MOBILEGL_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||||
// glTexSubImage from the client pointer instead of staging them through the
|
// glTexSubImage from the client pointer instead of staging them through the
|
||||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||||
// hatch).
|
// 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_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
// 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
|
// 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,
|
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||||
@@ -180,10 +204,10 @@ namespace MobileGL::MG_Config {
|
|||||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
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
|
// 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.
|
// 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
|
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||||
// unset picks the best supported tier.
|
// unset picks the best supported tier.
|
||||||
@@ -224,7 +248,7 @@ namespace MobileGL::MG_Config {
|
|||||||
// miscompiled shader: if a device ever renders differently with the cache
|
// miscompiled shader: if a device ever renders differently with the cache
|
||||||
// on, one run with this falsy says so.
|
// on, one run with this falsy says so.
|
||||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
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
|
// 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
|
// 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
|
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||||
@@ -236,7 +260,24 @@ namespace MobileGL::MG_Config {
|
|||||||
// the pre-emulation path, extension passthrough where it exists and
|
// the pre-emulation path, extension passthrough where it exists and
|
||||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||||
// the negative control the emulation is measured against.
|
// 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;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
+20
-15
@@ -162,34 +162,37 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
inline void InitFeatures() {
|
inline void InitFeatures() {
|
||||||
auto& features = MG_Config::Features;
|
auto& features = MG_Config::Features;
|
||||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
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.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)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||||
#endif
|
#endif
|
||||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||||
features.FixIterationRPSubgroupScratch =
|
features.MagmaFixIterationRPSubgroupScratch =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||||
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||||
features.AvoidSamplerMipmapMinFilter =
|
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||||
|
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||||
features.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_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.EsprytForceDepthStencilReadbackEmulation =
|
features.EsprytForceDepthStencilReadbackEmulation =
|
||||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
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.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
@@ -197,8 +200,10 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.AsyncOptimisticShaderStatus =
|
features.AsyncOptimisticShaderStatus =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||||
features.ViewportArrayEmulation =
|
features.EsprytViewportArrayEmulation =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||||
|
features.EsprytWidenPacked16Storage =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -1198,8 +1198,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
//
|
//
|
||||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||||
// above refuses to tell, just with an extra prerequisite met. Set
|
// above refuses to tell, just with an extra prerequisite met. Set
|
||||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
|
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||||
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
|
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
|
||||||
extensions.push_back(E_GL_ARB_texture_view);
|
extensions.push_back(E_GL_ARB_texture_view);
|
||||||
}
|
}
|
||||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||||
|
|||||||
@@ -10636,6 +10636,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// frame's ring high-water marks for slot reclamation.
|
// frame's ring high-water marks for slot reclamation.
|
||||||
BufferImpl::UboRingOnPresent();
|
BufferImpl::UboRingOnPresent();
|
||||||
BufferImpl::UnpackRingOnPresent();
|
BufferImpl::UnpackRingOnPresent();
|
||||||
|
BufferImpl::UploadRingOnPresent();
|
||||||
BufferImpl::TrimBufferPool();
|
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
|
// the driver fifteen more rectangles to rasterize against, and nothing in MobileGL has ever
|
||||||
// programmed the indexed state it would need.
|
// programmed the indexed state it would need.
|
||||||
Bool ViewportArrayEmulationEnabled() {
|
Bool ViewportArrayEmulationEnabled() {
|
||||||
return MG_Config::Features.ViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
|
return MG_Config::Features.EsprytViewportArrayEmulation != MG_Config::QuirkOverride::ForceOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool g_anyProgramRoutesViewportIndex = false;
|
Bool g_anyProgramRoutesViewportIndex = false;
|
||||||
@@ -75,13 +75,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() {
|
static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() {
|
||||||
// IsAngleLlvmpipeRenderer combined with the
|
// 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.
|
// both resolved in FillInGLESCapabilities.
|
||||||
return g_GLESCapabilities.AvoidSamplerMipmapMinFilter;
|
return g_GLESCapabilities.AvoidSamplerMipmapMinFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool ShouldAvoidExplicitLodBiasOnAngleLlvmpipe() {
|
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.
|
// feature toggle, both resolved in FillInGLESCapabilities.
|
||||||
return g_GLESCapabilities.AvoidExplicitLodBias;
|
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
|
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to
|
||||||
// spare and keeps consecutive staged blocks off each other's cache lines.
|
// spare and keeps consecutive staged blocks off each other's cache lines.
|
||||||
constexpr SizeT kUnpackRingAlignment = 64;
|
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 {
|
struct PersistentRingStore {
|
||||||
Uint id = 0;
|
Uint id = 0;
|
||||||
@@ -705,6 +710,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
kUnpackRingMaxBytes,
|
kUnpackRingMaxBytes,
|
||||||
kUnpackRingAlignment,
|
kUnpackRingAlignment,
|
||||||
"Texture unpack ring"};
|
"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
|
// 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
|
// seen): drop every handle without GL calls and re-arm creation. The
|
||||||
@@ -794,6 +814,96 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
bufferObject.MappedData() + start);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if (mapUsable) {
|
||||||
|
BindBufferId(TempBufferTarget, resource.id);
|
||||||
|
void* dst = g_GLESFuncs.glMapBufferRange(TempBufferTarget, (GLintptr)start,
|
||||||
|
(GLsizeiptr)size,
|
||||||
|
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
// EXT_buffer_storage bit values (same numeric values as the desktop ARB
|
||||||
// tokens); defined locally so this compiles regardless of which GLES headers
|
// tokens); defined locally so this compiles regardless of which GLES headers
|
||||||
// expose the EXT tokens.
|
// expose the EXT tokens.
|
||||||
@@ -941,11 +1051,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||||
resource->contextGeneration != g_bufferContextGeneration ||
|
resource->contextGeneration != g_bufferContextGeneration ||
|
||||||
!StorageMatches(*resource, bufferObject)) {
|
!StorageMatches(*resource, bufferObject)) {
|
||||||
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
resource->pendingRanges.Add({offset, offset + size});
|
resource->pendingRanges.Add({offset, offset + size});
|
||||||
return;
|
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. The zero-copy persistent store
|
||||||
|
// keeps the legacy immediate upload: draw-time sync never flushes
|
||||||
|
// ranges for it, and its mapping publishes writes by itself.
|
||||||
|
if ((resource->persistentMapped && resource->persistentPtr) ||
|
||||||
|
MG_Config::Features.EsprytDisableUploadRing) {
|
||||||
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
UploadRangeNow(*resource, bufferObject, offset, offset + size);
|
||||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
|
resource->pendingRanges.Add({offset, offset + size});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
||||||
@@ -956,6 +1082,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||||
resource->contextGeneration != g_bufferContextGeneration ||
|
resource->contextGeneration != g_bufferContextGeneration ||
|
||||||
!StorageMatches(*resource, bufferObject)) {
|
!StorageMatches(*resource, bufferObject)) {
|
||||||
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
|
resource->pendingRanges.Add(range);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same WAR-hazard rule as Ops_SubData: an immediate upload (mapped or
|
||||||
|
// glBufferSubData) can park the thread on Mali until the frames still
|
||||||
|
// referencing this store retire. Queue the range for the staged-copy
|
||||||
|
// flush at draw-time sync; only the zero-copy persistent store and the
|
||||||
|
// negative-control kill switch keep the immediate paths below.
|
||||||
|
if (!(resource->persistentMapped && resource->persistentPtr) &&
|
||||||
|
!MG_Config::Features.EsprytDisableUploadRing) {
|
||||||
|
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||||
resource->pendingRanges.Add(range);
|
resource->pendingRanges.Add(range);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1007,6 +1146,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
||||||
if (size == 0) return;
|
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);
|
BindBufferId(TempBufferTarget, resource->id);
|
||||||
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||||
GL_MAP_READ_BIT);
|
GL_MAP_READ_BIT);
|
||||||
@@ -1295,11 +1438,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->storageSize != bufferObject->GetSize()) {
|
resource->storageSize != bufferObject->GetSize()) {
|
||||||
RespecifyStorageNow(*resource, *bufferObject);
|
RespecifyStorageNow(*resource, *bufferObject);
|
||||||
} else if (!resource->pendingRanges.empty()) {
|
} else if (!resource->pendingRanges.empty()) {
|
||||||
for (const auto& range : resource->pendingRanges) {
|
FlushPendingRangesNow(*resource, *bufferObject);
|
||||||
const SizeT end = std::min(range.end, bufferObject->GetSize());
|
|
||||||
UploadRangeNow(*resource, *bufferObject, std::min(range.start, end), end);
|
|
||||||
}
|
|
||||||
resource->pendingRanges.clear();
|
|
||||||
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject->GetChangeSerial();
|
||||||
} else if (resource->syncedChangeSerial != bufferObject->GetChangeSerial()) {
|
} else if (resource->syncedChangeSerial != bufferObject->GetChangeSerial()) {
|
||||||
// Ops could not track some writes (e.g. the ops table was
|
// Ops could not track some writes (e.g. the ops table was
|
||||||
@@ -1621,6 +1760,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
"ring offset mask below requires power-of-two ring sizes");
|
"ring offset mask below requires power-of-two ring sizes");
|
||||||
static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0,
|
static_assert((kUnpackRingInitialBytes & (kUnpackRingInitialBytes - 1)) == 0,
|
||||||
"ring offset mask below requires power-of-two ring sizes");
|
"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));
|
const SizeT offset = static_cast<SizeT>(store.head & (store.size - 1));
|
||||||
if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) {
|
if (offset + alignedSize <= store.size && store.head + alignedSize - store.tail <= store.size) {
|
||||||
store.head += alignedSize;
|
store.head += alignedSize;
|
||||||
@@ -1760,7 +1901,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Bool UboRingAvailable() {
|
Bool UboRingAvailable() {
|
||||||
if (MG_Config::Features.DisableUboRing) return false;
|
if (MG_Config::Features.EsprytDisableUboRing) return false;
|
||||||
return RingAvailable(g_uboRing);
|
return RingAvailable(g_uboRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1773,7 +1914,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void UboRingOnPresent() { RingOnPresent(g_uboRing); }
|
void UboRingOnPresent() { RingOnPresent(g_uboRing); }
|
||||||
|
|
||||||
Bool UnpackRingAvailable() {
|
Bool UnpackRingAvailable() {
|
||||||
if (MG_Config::Features.DisableUnpackRing) return false;
|
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
|
||||||
return RingAvailable(g_unpackRing);
|
return RingAvailable(g_unpackRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1789,6 +1930,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
|
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
|
||||||
|
|
||||||
void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); }
|
void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); }
|
||||||
|
|
||||||
|
void UploadRingOnPresent() { RingOnPresent(g_uploadRing); }
|
||||||
} // namespace BufferImpl
|
} // namespace BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
@@ -3261,6 +3404,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
||||||
return data;
|
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 Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
|
||||||
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
|
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
|
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
|
||||||
@@ -6301,7 +6451,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
|
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
|
||||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||||
const std::map<String, String>& inputBlockRenames,
|
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,
|
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
|
||||||
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
|
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
|
||||||
String& outError) const {
|
String& outError) const {
|
||||||
@@ -6338,7 +6489,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const Int advertisedMaxSamples =
|
const Int advertisedMaxSamples =
|
||||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||||
// Armed by the EMULATION as well as by the missing extension, and the emulation is on
|
// 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
|
// 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
|
// the driver's INDEXED viewport state was never programmed by anything in MobileGL
|
||||||
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
|
// (SyncRenderState pushes index 0 and stops), so an extension-capable driver
|
||||||
@@ -6692,6 +6843,54 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &atomicCounterSpirv;
|
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::SpvcSession spvcSession(*effectiveSpirv,
|
||||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
|
||||||
@@ -7087,6 +7286,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
|
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
|
||||||
if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true;
|
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) {
|
if (anyStageCanDeclareBlocksInBothDirections) {
|
||||||
for (SizeT index = 0; index < shaderSpirvs.size(); ++index) {
|
for (SizeT index = 0; index < shaderSpirvs.size(); ++index) {
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
|
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
|
||||||
@@ -7281,6 +7492,39 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
|
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
|
||||||
esslKeyInputs.outputBlockRenames = &outputBlockRenames;
|
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;
|
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
|
||||||
|
|
||||||
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
|
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
|
||||||
@@ -7307,6 +7551,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
|
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
|
||||||
imageFormatBake, storageBlockBindingOverrides,
|
imageFormatBake, storageBlockBindingOverrides,
|
||||||
inputBlockRenames, outputBlockRenames,
|
inputBlockRenames, outputBlockRenames,
|
||||||
|
stripInputBlockLocations, stripOutputBlockLocations,
|
||||||
m_atomicCounterEsslBindingTop,
|
m_atomicCounterEsslBindingTop,
|
||||||
enableSpirvValidation, source,
|
enableSpirvValidation, source,
|
||||||
stageFlattenedXfbBlockNames,
|
stageFlattenedXfbBlockNames,
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
// 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.
|
// 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.
|
// restores the pre-emulation path as a negative control.
|
||||||
Bool ViewportArrayEmulationEnabled();
|
Bool ViewportArrayEmulationEnabled();
|
||||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||||
@@ -627,7 +627,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// which is what to watch if this ring ever shows up in an RSS regression: it
|
// 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.
|
// 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
|
// EXT_buffer_storage / fences are missing, the ES context is not current, or
|
||||||
// ring creation already failed under this context. Callers then upload from
|
// ring creation already failed under this context. Callers then upload from
|
||||||
// the client pointer exactly as before.
|
// the client pointer exactly as before.
|
||||||
@@ -642,6 +642,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Largest single staging request the ring can ever satisfy.
|
// Largest single staging request the ring can ever satisfy.
|
||||||
SizeT UnpackRingMaxBytes();
|
SizeT UnpackRingMaxBytes();
|
||||||
void UnpackRingOnPresent();
|
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 BufferImpl
|
||||||
|
|
||||||
namespace VertexArrayImpl {
|
namespace VertexArrayImpl {
|
||||||
@@ -1633,6 +1650,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||||
const std::map<String, String>& inputBlockRenames,
|
const std::map<String, String>& inputBlockRenames,
|
||||||
const std::map<String, String>& outputBlockRenames,
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
|
||||||
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||||
String& outSource,
|
String& outSource,
|
||||||
std::set<String>& outFlattenedXfbBlockNames,
|
std::set<String>& outFlattenedXfbBlockNames,
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
#include "Managers.h"
|
#include "Managers.h"
|
||||||
#include "MG_Backend/BackendObjects.h"
|
#include "MG_Backend/BackendObjects.h"
|
||||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||||
|
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
@@ -125,6 +127,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
requestedInternalFormat,
|
requestedInternalFormat,
|
||||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
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);
|
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -182,6 +192,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return options;
|
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,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
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,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType,
|
GLenum* outFormat, GLenum* outType,
|
||||||
TextureTarget target = TextureTarget::Unknown);
|
TextureTarget target = TextureTarget::Unknown);
|
||||||
@@ -523,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
// 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
|
// 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);
|
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
|
|||||||
@@ -624,7 +624,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (nonZeroIndirectBaseInstanceSupported) {
|
if (nonZeroIndirectBaseInstanceSupported) {
|
||||||
extensions.push_back(E_GL_ARB_base_instance);
|
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);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const RendererInfo& GetRendererIdentity();
|
const RendererInfo& GetRendererIdentity();
|
||||||
|
|
||||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
// 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
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||||
// the detected device support (passing an already-gated value is harmless).
|
// the detected device support (passing an already-gated value is harmless).
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
|
|||||||
@@ -10703,19 +10703,30 @@ void main() {
|
|||||||
"GetTexImage: failed to materialize pending clear for textureId=%d",
|
"GetTexImage: failed to materialize pending clear for textureId=%d",
|
||||||
textureObject->GetExternalIndex());
|
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 ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||||
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) {
|
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) {
|
||||||
const auto levelSize =
|
const auto levelSize =
|
||||||
textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
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
|
// Storage space: `resource` is the storage texture's, so a view's level and
|
||||||
// layer have to be shifted into its numbering (see ToStorageMipLevel).
|
// layer have to be shifted into its numbering (see ToStorageMipLevel).
|
||||||
const Int glArrayLayer = isCubeFace
|
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||||
? static_cast<Int>(textureUploadTarget) -
|
|
||||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX)
|
|
||||||
: 0;
|
|
||||||
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glArrayLayer);
|
|
||||||
const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level);
|
const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||||
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
// 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
|
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
||||||
@@ -10810,7 +10821,10 @@ void main() {
|
|||||||
// Storage space, as above: a texture view reads its own level 0 out of whichever level
|
// Storage space, as above: a texture view reads its own level 0 out of whichever level
|
||||||
// and layer of the parent it opened onto.
|
// and layer of the parent it opened onto.
|
||||||
copyRegion.imageSubresource.mipLevel = ToStorageMipLevel(textureObject.get(), level);
|
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.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
||||||
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
||||||
@@ -13200,8 +13214,8 @@ void main() {
|
|||||||
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
|
// 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
|
// 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.
|
// 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.
|
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS leaves it off to measure or dodge its GPU cost.
|
||||||
deviceFeatures.robustBufferAccess = MG_Config::Features.DisableRobustBufferAccess
|
deviceFeatures.robustBufferAccess = MG_Config::Features.MagmaDisableRobustBufferAccess
|
||||||
? VK_FALSE
|
? VK_FALSE
|
||||||
: supportedDeviceFeatures.robustBufferAccess;
|
: supportedDeviceFeatures.robustBufferAccess;
|
||||||
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
||||||
@@ -13576,13 +13590,13 @@ void main() {
|
|||||||
subgroupPropertyQuery.pNext = &subgroupProperties;
|
subgroupPropertyQuery.pNext = &subgroupProperties;
|
||||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
|
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
|
||||||
// Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the
|
// 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.
|
// disagree with the advertised capabilities.
|
||||||
const Bool usableSubgroups =
|
const Bool usableSubgroups =
|
||||||
subgroupProperties.subgroupSize > 0 &&
|
subgroupProperties.subgroupSize > 0 &&
|
||||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
||||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_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_nativeSubgroupSize = subgroupProperties.subgroupSize;
|
||||||
m_nativeSubgroupSupported = true;
|
m_nativeSubgroupSupported = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Native subgroup topology, queried at device creation for the compute-module
|
// Native subgroup topology, queried at device creation for the compute-module
|
||||||
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
||||||
// stage flag; 0 / false when the device has no usable compute subgroups or
|
// 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;
|
Uint32 m_nativeSubgroupSize = 0;
|
||||||
Bool m_nativeSubgroupSupported = false;
|
Bool m_nativeSubgroupSupported = false;
|
||||||
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||||
|
|||||||
@@ -39,18 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||||
!MG_Config::Features.DisableSubgroup;
|
!MG_Config::Features.MagmaDisableSubgroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||||
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||||
// grows one under-declared array; every other module passes through untouched.
|
// 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;
|
MG_Config::QuirkOverride::ForceOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Bool ShouldFixIterationRPBarrier() {
|
inline Bool ShouldFixIterationRPBarrier() {
|
||||||
return MG_Config::Features.IterationRPFixBarrier;
|
return MG_Config::Features.MagmaIterationRPFixBarrier;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Bool ShouldDeriveNumSubgroups() {
|
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
|
// 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
|
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||||
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
// 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
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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.
|
// 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,
|
Bool ValidateTextureImageQuery(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
|
||||||
TextureInputFormat textureInputFormat, TexturePixelDataType texturePixelDataType,
|
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)) {
|
if (!TextureImpl::ValidateTextureObject(textureObject)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -5196,12 +5209,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tightly packed, and summed over every face because a cube map query returns all
|
// Tightly packed, and summed over every face because a whole-level cube map query
|
||||||
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
// returns all six - unless the caller named a single face, which is what a non-zero
|
||||||
// could not have fit under any packing.
|
// 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,
|
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||||
texturePixelDataType, texelSize) *
|
texturePixelDataType, texelSize) *
|
||||||
uploadTargets.size();
|
imageCount;
|
||||||
|
|
||||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -5274,9 +5289,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
|
isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
|
||||||
: bindingSlot.GetBoundObject();
|
: 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,
|
return ValidateTextureImageQuery(textureObject, level, textureInputFormat, texturePixelDataType, -1, pixels,
|
||||||
"GetTexImage_State");
|
"GetTexImage_State",
|
||||||
|
IsCubeMapFaceUploadTarget(textureUploadTarget) ? 1u : 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What this helper can and cannot answer.
|
// 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) {
|
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
if (!textureObject) return;
|
||||||
@@ -6431,16 +6468,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
__func__)) {
|
__func__)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
|
||||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
bufSize, pixels, __func__);
|
||||||
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__);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels) {
|
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 auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||||
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && zoffset == 0 &&
|
// On a cube map, z is the FACE axis. A cube map's level is stored per face, so its level
|
||||||
width == texelSize.x() && height == texelSize.y() &&
|
// size reads z = 1 whichever face named it - but GL 4.6 core 8.11.4 addresses the six faces
|
||||||
depth == texelSize.z();
|
// 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) {
|
if (!isFullLevelRead) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -6495,7 +6534,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
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
|
// A buffer texture carries none of the sampler or level state these queries report. Reached by
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/SsboDeclarationFormScenario.cpp
|
Scenarios/SsboDeclarationFormScenario.cpp
|
||||||
Scenarios/Glsl420DeclarationScenario.cpp
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||||
|
Scenarios/UnlocatedIoBlockScenario.cpp
|
||||||
Scenarios/TessellationDrawModeScenario.cpp
|
Scenarios/TessellationDrawModeScenario.cpp
|
||||||
Scenarios/GeometryDrawModeScenario.cpp
|
Scenarios/GeometryDrawModeScenario.cpp
|
||||||
Scenarios/PostLinkAttachScenario.cpp
|
Scenarios/PostLinkAttachScenario.cpp
|
||||||
@@ -105,6 +106,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
Scenarios/CopyImageLayeredScenario.cpp
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
|
Scenarios/CopyImagePacked16Scenario.cpp
|
||||||
Scenarios/TextureViewScenario.cpp
|
Scenarios/TextureViewScenario.cpp
|
||||||
Scenarios/PackedWordReadbackScenario.cpp
|
Scenarios/PackedWordReadbackScenario.cpp
|
||||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||||
@@ -286,9 +288,9 @@ if (MOBILEGL_ITEST_VK_ICD)
|
|||||||
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
||||||
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
||||||
list(APPEND MGL_ITEST_VULKAN_ENV
|
list(APPEND MGL_ITEST_VULKAN_ENV
|
||||||
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
"MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||||
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
|
"MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1"
|
||||||
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
|
"MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -351,7 +353,18 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
|
|||||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
||||||
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
|
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})
|
||||||
|
|
||||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
set(MGL_ITEST_TIMEOUT 120)
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
@@ -418,6 +431,23 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
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.
|
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||||
#
|
#
|
||||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||||
@@ -514,3 +544,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_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}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -225,4 +225,47 @@ void main() {
|
|||||||
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
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. The DirectGLES backend 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. Offset 4 pins the other
|
||||||
|
// direction: the flush must not clobber GPU results outside the written range.
|
||||||
|
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
// DirectGLES-only for now. DirectVulkan fails this case with or without the upload
|
||||||
|
// ring, on revisions that predate it: its buffer uploads submit immediately while the
|
||||||
|
// dispatch sits in the deferred frame command buffer, so the GPU increments the
|
||||||
|
// RESEEDED value (reads 4242 + increments instead of 4242) - a pre-existing
|
||||||
|
// upload-vs-recorded-work ordering gap in that backend, kept visible here rather than
|
||||||
|
// silently absorbed. Un-skip once DirectVulkan orders app uploads against already
|
||||||
|
// recorded GPU work.
|
||||||
|
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||||
|
GTEST_SKIP() << "SubData-after-dispatch ordering is a known DirectVulkan gap; this case pins the "
|
||||||
|
"DirectGLES readback pre-flush only";
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
} // 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
|
||||||
@@ -137,7 +137,7 @@ namespace MGITest {
|
|||||||
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
||||||
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
|
// 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 -
|
// 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;
|
// this probe deliberately does not model. The width only gates the domain;
|
||||||
// lane placement and group counts still come from observed values alone.
|
// lane placement and group counts still come from observed values alone.
|
||||||
bool SubgroupWidthInSourceDomain() const {
|
bool SubgroupWidthInSourceDomain() const {
|
||||||
|
|||||||
@@ -32,14 +32,20 @@
|
|||||||
// too, so the per-slice branch that exists for exactly this case was unreachable and every
|
// 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.
|
// 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
|
// The first seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array
|
||||||
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
|
// with its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and
|
||||||
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
// 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
|
// 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
|
// 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
|
// 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".
|
// 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
|
// 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
|
// 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
|
// 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.
|
// mismatch means a real miss rather than rounding.
|
||||||
constexpr Rgba8 kClearColor{17, 68, 187, 255};
|
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
|
// 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
|
// 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").
|
// rejected" is distinguishable from "the first draw never happened").
|
||||||
@@ -357,6 +367,27 @@ void main()
|
|||||||
return texture;
|
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 -
|
// 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
|
// 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.
|
// 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() {
|
::testing::AssertionResult FramebufferIsComplete() {
|
||||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
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.
|
// Every face, read back through an FBO that names THAT face.
|
||||||
//
|
//
|
||||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
|
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): when this case was written
|
||||||
// where only +X had been cleared, that spelling returned the cleared colour for all
|
// that spelling could not see per-face state on DirectVulkan at all - measured against
|
||||||
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
|
// a tree where only +X had been cleared it returned the cleared colour for all six
|
||||||
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
|
// faces - so a case built on it would have been unfalsifiable. That is a readback
|
||||||
// else, and the pending clear is long gone by now (materialised and popped above), so
|
// defect rather than an attachment one, and case (8) below is where it is pinned and
|
||||||
// this readback cannot alter what it is measuring.
|
// fixed; this case keeps the independent spelling deliberately, because it must go on
|
||||||
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
// 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) {
|
for (int face = 0; face < 6; ++face) {
|
||||||
const GLuint faceFbo = TrackFramebuffer();
|
const GLuint faceFbo = TrackFramebuffer();
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
||||||
@@ -958,5 +1020,114 @@ void main()
|
|||||||
Gl().EndFrame();
|
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
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,526 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
|
||||||
|
// QUALIFIER REMOVED.
|
||||||
|
//
|
||||||
|
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
|
||||||
|
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
|
||||||
|
// stages compile, the program links with an empty info log, the draw runs, and the consuming
|
||||||
|
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
|
||||||
|
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
|
||||||
|
// from the blocks (and changing nothing else) makes the same program carry its payload. The
|
||||||
|
// locations are not the application's in the first place: these shaders declare none, and
|
||||||
|
// glslang's cross-stage IO resolver invents them.
|
||||||
|
//
|
||||||
|
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
|
||||||
|
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
|
||||||
|
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
|
||||||
|
// location must still deliver its payload end to end. It is the assertion the affected device
|
||||||
|
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
|
||||||
|
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
|
||||||
|
// cannot reproduce the defect at all.
|
||||||
|
//
|
||||||
|
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
|
||||||
|
// entry, because llvmpipe carries a located block correctly and the driver POST would
|
||||||
|
// therefore never turn the emulation on here. The SAME cases also run under the ambient
|
||||||
|
// registrations with the emulation off, so both spellings of the interface are covered and a
|
||||||
|
// regression in either shows up.
|
||||||
|
//
|
||||||
|
// Colour code, so a failure names its own cause:
|
||||||
|
// green - the payload crossed all four stage boundaries, which is the pass.
|
||||||
|
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
||||||
|
// backend program was rejected and every draw became a no-op).
|
||||||
|
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
||||||
|
// failure is not about interface blocks.
|
||||||
|
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
||||||
|
// zeroed. That is what an interface whose two ends stopped matching looks like.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// NOTHING in these five stages declares a location. Every location the emitted ESSL
|
||||||
|
// carries is invented by the cross-stage resolver, which is exactly the shape the
|
||||||
|
// affected driver mishandles and exactly what the strip removes.
|
||||||
|
//
|
||||||
|
// Two members per block, of different types, because an interface that is matched by
|
||||||
|
// name and member sequence rather than by location has to agree on the sequence too -
|
||||||
|
// a repair that silently reordered or dropped a member would still light up green with
|
||||||
|
// one member in the block.
|
||||||
|
const char* const kVertexSource = R"(#version 420 core
|
||||||
|
out VsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} vs_out;
|
||||||
|
out float vs_tcs_alive;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
vs_out.tint = vec2(0.25, 0.5);
|
||||||
|
vs_tcs_alive = 1.0;
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kTessControlSource = R"(#version 420 core
|
||||||
|
layout(vertices = 1) out;
|
||||||
|
in VsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tcs_in[];
|
||||||
|
in float vs_tcs_alive[];
|
||||||
|
out TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tcs_out[];
|
||||||
|
out float tcs_tes_alive[];
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
||||||
|
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
|
||||||
|
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelOuter[3] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
gl_TessLevelInner[1] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Distinct block names, so this case is about the LOCATION and nothing else; the
|
||||||
|
// one-name-in-both-directions shape is the case below.
|
||||||
|
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tes_in[];
|
||||||
|
in float tcs_tes_alive[];
|
||||||
|
out TesData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tes_out;
|
||||||
|
out float tes_gs_alive;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_out.payload = tes_in[0].payload;
|
||||||
|
tes_out.tint = tes_in[0].tint;
|
||||||
|
tes_gs_alive = tcs_tes_alive[0];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The 420pack shape: ONE name for the block this stage consumes and the block it
|
||||||
|
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
|
||||||
|
// the rename gives the two blocks one spelling per producing stage, the strip takes
|
||||||
|
// their locations off, and the interfaces still have to meet.
|
||||||
|
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tes_in[];
|
||||||
|
in float tcs_tes_alive[];
|
||||||
|
out TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} tes_out;
|
||||||
|
out float tes_gs_alive;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_out.payload = tes_in[0].payload;
|
||||||
|
tes_out.tint = tes_in[0].tint;
|
||||||
|
tes_gs_alive = tcs_tes_alive[0];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// One geometry source per evaluation stage, because the block it consumes is named
|
||||||
|
// after the block the evaluation stage produced.
|
||||||
|
const char* const kDistinctGeometrySource = R"(#version 420 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
in TesData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} gs_in[];
|
||||||
|
in float tes_gs_alive[];
|
||||||
|
out GsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} gs_out;
|
||||||
|
out float gs_fs_alive;
|
||||||
|
void EmitCorner(vec2 corner)
|
||||||
|
{
|
||||||
|
gs_out.payload = gs_in[0].payload;
|
||||||
|
gs_out.tint = gs_in[0].tint;
|
||||||
|
gs_fs_alive = tes_gs_alive[0];
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
EmitCorner(vec2(-1.0, -1.0));
|
||||||
|
EmitCorner(vec2(-1.0, 1.0));
|
||||||
|
EmitCorner(vec2( 1.0, -1.0));
|
||||||
|
EmitCorner(vec2( 1.0, 1.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kCollidingGeometrySource = R"(#version 420 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} gs_in[];
|
||||||
|
in float tes_gs_alive[];
|
||||||
|
out GsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} gs_out;
|
||||||
|
out float gs_fs_alive;
|
||||||
|
void EmitCorner(vec2 corner)
|
||||||
|
{
|
||||||
|
gs_out.payload = gs_in[0].payload;
|
||||||
|
gs_out.tint = gs_in[0].tint;
|
||||||
|
gs_fs_alive = tes_gs_alive[0];
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
EmitCorner(vec2(-1.0, -1.0));
|
||||||
|
EmitCorner(vec2(-1.0, 1.0));
|
||||||
|
EmitCorner(vec2( 1.0, -1.0));
|
||||||
|
EmitCorner(vec2( 1.0, 1.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Green ONLY when both block members arrived: a repair that kept the first member and
|
||||||
|
// lost the second would otherwise pass. Red when the plain varying is missing too, so
|
||||||
|
// "the pipeline is broken" and "the block is broken" cannot be confused.
|
||||||
|
const char* const kFragmentSource = R"(#version 420 core
|
||||||
|
in GsData {
|
||||||
|
vec4 payload;
|
||||||
|
vec2 tint;
|
||||||
|
} fs_in;
|
||||||
|
in float gs_fs_alive;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
if (gs_fs_alive <= 0.5) {
|
||||||
|
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
|
||||||
|
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
fragColor = fs_in.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class UnlocatedIoBlockScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
if (!BackendHostsTessellationAndGeometry()) {
|
||||||
|
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
||||||
|
<< "carry a block through";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
for (const GLuint program : m_programs) {
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
m_programs.clear();
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_vao = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
|
||||||
|
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
|
||||||
|
// frontend constants, so a stack with no five-stage pipeline is recognised by
|
||||||
|
// trying to build one, not by asking.
|
||||||
|
static bool BackendHostsTessellationAndGeometry() {
|
||||||
|
GLint maxTessGenLevel = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
|
GLint maxGeometryOutputVertices = 0;
|
||||||
|
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
||||||
|
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||||
|
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
||||||
|
geometrySource, kFragmentSource};
|
||||||
|
|
||||||
|
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
shaders[i] = glCreateShader(stages[i]);
|
||||||
|
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
||||||
|
glCompileShader(shaders[i]);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (!compiled) {
|
||||||
|
m_buildLog = InfoLog(shaders[i], true);
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
if (shader != 0) glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
if (!linked) {
|
||||||
|
m_buildLog = InfoLog(program, false);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_programs.push_back(program);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
||||||
|
// than something that could be mistaken for a zeroed payload.
|
||||||
|
Rgba8 DrawAndReadCentre(GLuint program) const {
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(program);
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||||
|
glDrawArrays(GL_PATCHES, 0, 1);
|
||||||
|
|
||||||
|
Rgba8 pixel{};
|
||||||
|
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||||
|
return pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsGreen(const Rgba8& pixel) {
|
||||||
|
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& BuildLog() const { return m_buildLog; }
|
||||||
|
|
||||||
|
// The library log this process is writing, or an empty path when none was
|
||||||
|
// configured. MOBILEGL_LOG_FILE_PATH is read at log-init, before anything this
|
||||||
|
// fixture can reach, so the ctest entry sets it and this only reads it back.
|
||||||
|
static std::filesystem::path LibraryLogPath() {
|
||||||
|
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||||
|
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||||
|
: std::filesystem::path();
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many bytes the library log already holds. Everything this fixture asserts on
|
||||||
|
// is searched from here forward, because the file is APPENDED to by every process
|
||||||
|
// in the lane and a line left behind by an earlier one would otherwise satisfy the
|
||||||
|
// assertion without this process having done anything at all.
|
||||||
|
static std::uintmax_t LibraryLogSize() {
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return 0;
|
||||||
|
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||||
|
return ec ? 0 : size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||||
|
const std::filesystem::path path = LibraryLogPath();
|
||||||
|
if (path.empty()) return {};
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file.good()) return {};
|
||||||
|
file.seekg(static_cast<std::streamoff>(offset));
|
||||||
|
return std::string((std::istreambuf_iterator<char>(file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLenum FirstGLError() {
|
||||||
|
const GLenum first = glGetError();
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string InfoLog(GLuint object, bool isShader) {
|
||||||
|
GLint length = 0;
|
||||||
|
if (isShader) {
|
||||||
|
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
} else {
|
||||||
|
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
}
|
||||||
|
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
if (isShader) {
|
||||||
|
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
} else {
|
||||||
|
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
}
|
||||||
|
return std::string(log.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||||
|
if (program == 0) {
|
||||||
|
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||||
|
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||||
|
<< BuildLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndReadCentre(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(IsGreen(centre))
|
||||||
|
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
|
||||||
|
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
|
||||||
|
"member arrived wrong, i.e. the interface stopped matching)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two repairs together. The rename is what makes the evaluation stage's two
|
||||||
|
// TcsData blocks one spelling per producing stage; the strip then takes the locations
|
||||||
|
// off the names the rename just settled. Either one alone leaves a working program on
|
||||||
|
// these machines, so this case is here to catch the two of them disagreeing.
|
||||||
|
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
||||||
|
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||||
|
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||||
|
<< BuildLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
||||||
|
ASSERT_NE(program, 0u)
|
||||||
|
<< "an interface block name reused across the two directions of one stage is legal "
|
||||||
|
"desktop GLSL, but the program did not build: "
|
||||||
|
<< BuildLog();
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndReadCentre(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(IsGreen(centre))
|
||||||
|
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE ONE CASE THAT CAN FAIL WHEN THE REPAIR SILENTLY STOPS BEING ARMED.
|
||||||
|
//
|
||||||
|
// Everything above renders green on llvmpipe whether the blocks were stripped or not -
|
||||||
|
// this machine carries a located block correctly - so those cases pin that the strip
|
||||||
|
// does no HARM and can say nothing about whether it happened. That leaves the arming
|
||||||
|
// itself untested, and the arming is where the cheap mistake lives: Loader.cpp maps
|
||||||
|
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS onto the capability INVERTED (forcing the
|
||||||
|
// emulation on means declaring located blocks UNSUPPORTED), and a one-line swap of
|
||||||
|
// those two arms would disable the device repair with every test here still green.
|
||||||
|
//
|
||||||
|
// So this case asserts a LIBRARY OBSERVABLE against the environment, the shape
|
||||||
|
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration uses: the environment
|
||||||
|
// says the emulation is pinned on, therefore the library must SAY it stripped
|
||||||
|
// something. The observable is the latched MGLOG_I DirectGLES emits the first time the
|
||||||
|
// pass fires (Managers.cpp); it is INFO rather than DEBUG precisely so that this
|
||||||
|
// assertion is possible in the builds CI runs.
|
||||||
|
//
|
||||||
|
// Two things it deliberately does NOT do: it does not read MG_Config (on Android this
|
||||||
|
// module links the shipping library, which exports nothing internal - the reason
|
||||||
|
// ViewportArrayScenario's control moved to the environment), and it does not trust the
|
||||||
|
// whole log file, only the bytes appended after this test started.
|
||||||
|
TEST_F(UnlocatedIoBlockScenario, TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS") != AmbientQuirk::On) {
|
||||||
|
GTEST_SKIP() << "this case needs the emulation pinned ON for the whole process, which "
|
||||||
|
"is what the UnlocatedIoBlocks. ctest entry does with "
|
||||||
|
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1; with the variable unset the "
|
||||||
|
"driver POST decides, and on this machine it decides the blocks are "
|
||||||
|
"fine - so there would be nothing to observe";
|
||||||
|
}
|
||||||
|
if (LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned on but "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||||
|
"record that it stripped anything; the UnlocatedIoBlocks. ctest entry "
|
||||||
|
"sets both";
|
||||||
|
}
|
||||||
|
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||||
|
GTEST_SKIP() << "the strip is DirectGLES's; " << Gl().BackendName()
|
||||||
|
<< " hands the module to the driver as SPIR-V, where Location is how "
|
||||||
|
"interfaces are matched";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||||
|
// this process wrote. The latch means it is emitted at the FIRST stage of the
|
||||||
|
// FIRST affected program, which is inside the build below.
|
||||||
|
const std::uintmax_t before = LibraryLogSize();
|
||||||
|
|
||||||
|
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||||
|
if (program == 0) {
|
||||||
|
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||||
|
<< Gl().BackendName() << ", so nothing would arm the strip: " << BuildLog();
|
||||||
|
}
|
||||||
|
// Drawn as well as built, so a stack that defers its backend program to first use
|
||||||
|
// still reaches the transpile this is asserting about.
|
||||||
|
const Rgba8 centre = DrawAndReadCentre(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(IsGreen(centre)) << "the pinned-on lane did not even render correctly: " << centre;
|
||||||
|
|
||||||
|
const std::string appended = LibraryLogSince(before);
|
||||||
|
EXPECT_NE(appended.find("WITHOUT their layout(location) qualifier"), std::string::npos)
|
||||||
|
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned ON, a five-stage program with four "
|
||||||
|
"interface-block boundaries was built and drawn, and DirectGLES never reported "
|
||||||
|
"stripping a single location. The emulation is not armed - check the override "
|
||||||
|
"mapping in Loader.cpp (it is inverted on purpose) and the arming gate in "
|
||||||
|
"Managers.cpp. Log appended by this test:\n"
|
||||||
|
<< appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -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
|
// 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
|
// 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
|
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
|
||||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
// 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
|
// 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
|
// 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
|
// ambient ctest entries, or the binary run straight from a device shell - the
|
||||||
// emulation is on and this case skips.
|
// 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 "
|
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 "
|
"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);
|
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||||
|
|||||||
@@ -97,27 +97,31 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_storageOwner->HasFixedSampleLocations();
|
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 {
|
TextureUploadTarget TextureObjectView::ToOwnerUploadTarget(TextureUploadTarget viewTarget) const {
|
||||||
const auto& ownerTargets = m_storageOwner->GetUploadTargets();
|
const auto& ownerTargets = m_storageOwner->GetUploadTargets();
|
||||||
MOBILEGL_ASSERT(!ownerTargets.empty(), "TextureObjectView: storage owner has no upload target");
|
MOBILEGL_ASSERT(!ownerTargets.empty(), "TextureObjectView: storage owner has no upload target");
|
||||||
if (ownerTargets.size() == 1) {
|
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];
|
return ownerTargets[0];
|
||||||
}
|
}
|
||||||
// The owner is a cube map: six independent blobs, one per face, and the view's layer
|
// The owner is a cube map: six independent blobs, one per face, and the layer this view
|
||||||
// index selects among them. A cube-map view of a cube map maps face to face; any other
|
// target names selects among them. A cube-map view of a cube map maps face to face; any
|
||||||
// view target addresses layers, which for a cube-map owner ARE its faces.
|
// other view target addresses layers, which for a cube-map owner ARE its faces.
|
||||||
const Uint faceCount = static_cast<Uint>(ownerTargets.size());
|
const Uint faceCount = static_cast<Uint>(ownerTargets.size());
|
||||||
Uint face = m_viewMinLayer;
|
return ownerTargets[std::min(ViewLayerIndex(viewTarget), faceCount - 1)];
|
||||||
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)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IntVec3 TextureObjectView::ToViewLevelSize(const IntVec3& ownerLevelSize) const {
|
IntVec3 TextureObjectView::ToViewLevelSize(const IntVec3& ownerLevelSize) const {
|
||||||
@@ -151,7 +155,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SizeT TextureObjectView::LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const {
|
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());
|
const LayerAxis ownerAxis = LayerAxisOf(m_storageOwner->GetTarget());
|
||||||
if (ownerAxis == LayerAxis::None) {
|
if (ownerAxis == LayerAxis::None) {
|
||||||
// A cube-map owner keeps each face in its OWN blob, and ToOwnerUploadTarget already
|
// 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.x(), 0)) *
|
: static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
|
||||||
static_cast<SizeT>(std::max(ownerSize.y(), 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;
|
return offset < ownerBytes ? offset : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntVec3 TextureObjectView::ToOwnerRegionOffset(const IntVec3& viewOffset) const {
|
IntVec3 TextureObjectView::ToOwnerRegionOffset(TextureUploadTarget viewTarget, const IntVec3& viewOffset) const {
|
||||||
if (m_viewMinLayer == 0) return viewOffset;
|
const Uint layerIndex = ViewLayerIndex(viewTarget);
|
||||||
|
if (layerIndex == 0) return viewOffset;
|
||||||
IntVec3 offset = viewOffset;
|
IntVec3 offset = viewOffset;
|
||||||
// The dirty region is recorded in the OWNER's blob coordinates - that is the space its
|
// 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:
|
// 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())) {
|
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
|
||||||
case LayerAxis::Y:
|
case LayerAxis::Y:
|
||||||
offset.y() += static_cast<Int>(m_viewMinLayer);
|
offset.y() += static_cast<Int>(layerIndex);
|
||||||
break;
|
break;
|
||||||
case LayerAxis::Z:
|
case LayerAxis::Z:
|
||||||
offset.z() += static_cast<Int>(m_viewMinLayer);
|
offset.z() += static_cast<Int>(layerIndex);
|
||||||
break;
|
break;
|
||||||
case LayerAxis::None:
|
case LayerAxis::None:
|
||||||
break;
|
break;
|
||||||
@@ -300,7 +313,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
IntVec3 size) {
|
IntVec3 size) {
|
||||||
if (m_ownerMipmap == nullptr) return;
|
if (m_ownerMipmap == nullptr) return;
|
||||||
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
|
||||||
ToOwnerRegionOffset(offset), size);
|
ToOwnerRegionOffset(uploadTarget, offset), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
MipmapDirtyRegion TextureObjectView::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
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
|
// 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.
|
// the mapping is "the owner's only target" unless one of the two sides is a cube map.
|
||||||
TextureUploadTarget ToOwnerUploadTarget(TextureUploadTarget viewTarget) const;
|
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; }
|
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
|
// 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
|
// 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
|
// 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.
|
// separate blobs that ToOwnerUploadTarget already selects between.
|
||||||
SizeT LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const;
|
SizeT LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const;
|
||||||
// A dirty-region origin moved from the view's layer space into the owner's.
|
// A dirty-region origin moved from the view's layer space into the owner's. Takes the view
|
||||||
IntVec3 ToOwnerRegionOffset(const IntVec3& viewOffset) const;
|
// 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;
|
SharedPtr<ITextureObject> m_storageOwner;
|
||||||
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in
|
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -14,9 +15,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <Init.h>
|
||||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
|
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
||||||
|
|
||||||
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
|
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
|
||||||
// a GLESFunctionsTable populated with captureless lambdas backed by the file-scope
|
// a GLESFunctionsTable populated with captureless lambdas backed by the file-scope
|
||||||
@@ -25,6 +28,13 @@
|
|||||||
// ANGLE-style baseInstance-leaking driver, or a failing one.
|
// ANGLE-style baseInstance-leaking driver, or a failing one.
|
||||||
namespace {
|
namespace {
|
||||||
struct FakeDriverState {
|
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.
|
// Behavior knobs, configured per test before running the probe.
|
||||||
GLint maxVertexSsboBlocks = 4;
|
GLint maxVertexSsboBlocks = 4;
|
||||||
GLint glesMajorVersion = 3;
|
GLint glesMajorVersion = 3;
|
||||||
@@ -433,6 +443,52 @@ namespace {
|
|||||||
};
|
};
|
||||||
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
||||||
funcs.glBindRenderbuffer = [](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.glRenderbufferStorage = [](GLenum, GLenum, GLsizei, GLsizei) {};
|
||||||
funcs.glFramebufferRenderbuffer = [](GLenum, GLenum, GLenum, GLuint) {};
|
funcs.glFramebufferRenderbuffer = [](GLenum, GLenum, GLenum, GLuint) {};
|
||||||
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||||
@@ -1347,3 +1403,151 @@ TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
|||||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
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";
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
|||||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeCopyImageMirrorsPacked16FieldOrder;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
||||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||||
|
|
||||||
@@ -107,6 +108,28 @@ namespace {
|
|||||||
bool blitIgnoresDestinationLayer = false;
|
bool blitIgnoresDestinationLayer = false;
|
||||||
bool blitIgnoresSourceLayer = 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 ---------------------------------------------
|
// ---- object bookkeeping ---------------------------------------------
|
||||||
GLenum pendingError = GL_NO_ERROR;
|
GLenum pendingError = GL_NO_ERROR;
|
||||||
GLuint nextShaderId = 1;
|
GLuint nextShaderId = 1;
|
||||||
@@ -136,7 +159,34 @@ namespace {
|
|||||||
std::map<GLuint, std::array<GLubyte, 2>> arrayLayerFill;
|
std::map<GLuint, std::array<GLubyte, 2>> arrayLayerFill;
|
||||||
// framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached.
|
// framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached.
|
||||||
std::map<GLuint, std::pair<GLuint, GLint>> framebufferLayerAttachment;
|
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 boundArrayTexture = 0;
|
||||||
|
GLuint boundTexture2D = 0;
|
||||||
GLuint boundDrawFramebuffer = 0;
|
GLuint boundDrawFramebuffer = 0;
|
||||||
GLuint boundReadFramebuffer = 0;
|
GLuint boundReadFramebuffer = 0;
|
||||||
|
|
||||||
@@ -164,6 +214,17 @@ namespace {
|
|||||||
return haystack.find(needle) != std::string::npos;
|
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.
|
// Every `image2D <name>` the program declares, across all its stages.
|
||||||
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
||||||
std::vector<std::string> names;
|
std::vector<std::string> names;
|
||||||
@@ -401,6 +462,7 @@ namespace {
|
|||||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = 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_ARRAY) g_fake.boundArrayTexture = texture;
|
||||||
|
if (target == GL_TEXTURE_2D) g_fake.boundTexture2D = texture;
|
||||||
};
|
};
|
||||||
funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) {
|
funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) {
|
||||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.arrayLayerFill[g_fake.boundArrayTexture] = {0, 0};
|
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) {
|
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||||
for (GLsizei i = 0; i < n; ++i) {
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
if (textures[i] != 0) --g_fake.aliveTextures;
|
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) {
|
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
|
||||||
@@ -425,9 +495,63 @@ namespace {
|
|||||||
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
|
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
|
||||||
static_cast<GLenum>(param);
|
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,
|
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||||
const void*) {};
|
const void*) {};
|
||||||
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||||
@@ -446,11 +570,16 @@ namespace {
|
|||||||
g_fake.boundReadFramebuffer = framebuffer;
|
g_fake.boundReadFramebuffer = framebuffer;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
funcs.glFramebufferTexture2D = [](GLenum target, GLenum, GLenum, GLuint texture, GLint) {
|
||||||
funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) {
|
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
|
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||||
: g_fake.boundDrawFramebuffer;
|
: g_fake.boundDrawFramebuffer;
|
||||||
g_fake.framebufferLayerAttachment[framebuffer] = {texture, layer};
|
g_fake.framebufferLayerAttachment[framebuffer] = {texture, layer};
|
||||||
|
g_fake.framebufferLayerLevel[framebuffer] = level;
|
||||||
};
|
};
|
||||||
funcs.glReadBuffer = [](GLenum) {};
|
funcs.glReadBuffer = [](GLenum) {};
|
||||||
// The defect itself: the source layer is read from where the READ framebuffer says (unless
|
// 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) {
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||||
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
||||||
|
g_fake.framebufferLayerLevel.erase(framebuffers[i]);
|
||||||
|
g_fake.framebuffer2DAttachment.erase(framebuffers[i]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||||
@@ -561,11 +692,54 @@ namespace {
|
|||||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels) {
|
void* pixels) {
|
||||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
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
|
// A read framebuffer naming a plain 2D texture that holds a 5551 word is the
|
||||||
// layered-blit probe asking what that layer holds, and its bytes have nothing to do
|
// packed16 probe reading its copy destination. The driver decodes its OWN storage
|
||||||
// with the pass/fail texel encoding the image probes below share.
|
// 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);
|
if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer);
|
||||||
layered != g_fake.framebufferLayerAttachment.end()) {
|
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 auto& fill = g_fake.arrayLayerFill[layered->second.first];
|
||||||
const GLint layer = layered->second.second;
|
const GLint layer = layered->second.second;
|
||||||
const GLubyte value =
|
const GLubyte value =
|
||||||
@@ -626,6 +800,8 @@ TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
|||||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
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
|
// 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();
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ add_executable(
|
|||||||
FlattenFloat64StorageBlockTest.cpp
|
FlattenFloat64StorageBlockTest.cpp
|
||||||
FlattenXfbInterfaceBlocksTest.cpp
|
FlattenXfbInterfaceBlocksTest.cpp
|
||||||
UniquifyIoBlockNamesTest.cpp
|
UniquifyIoBlockNamesTest.cpp
|
||||||
|
StripIoBlockLocationsTest.cpp
|
||||||
LowerViewportIndexTest.cpp
|
LowerViewportIndexTest.cpp
|
||||||
ClampMultisampleFetchTest.cpp
|
ClampMultisampleFetchTest.cpp
|
||||||
LegalizeResourceArrayIndexTest.cpp
|
LegalizeResourceArrayIndexTest.cpp
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -1001,6 +1001,20 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
|||||||
v.outputBlockRenames = &otherIoBlockRenames;
|
v.outputBlockRenames = &otherIoBlockRenames;
|
||||||
variants.emplace_back("outputBlockRenames(other target)", BuildEsslTranslationKey(v));
|
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;
|
EsslTranslationKeyInputs v = base;
|
||||||
v.enableSpirvValidation = true;
|
v.enableSpirvValidation = true;
|
||||||
|
|||||||
@@ -2052,6 +2052,81 @@ TEST_F(TextureTest, GetTextureSubImageRejectsPartialReadbackForNow) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
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) {
|
TEST_F(TextureTest, TextureParameteriAndBindTextureUnitAreDirectStateAccess) {
|
||||||
GLuint texture = 0;
|
GLuint texture = 0;
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
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
|
// 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
|
// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with
|
||||||
// GL_RGB565 was rejected as an unknown internal format.
|
// GL_RGB565 was rejected as an unknown internal format.
|
||||||
|
|||||||
@@ -505,4 +505,47 @@ namespace {
|
|||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
|
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
|
} // namespace
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Util::BackendLoader {
|
namespace MobileGL::MG_Util::BackendLoader {
|
||||||
static Bool UseAngle() {
|
static Bool UseAngle() {
|
||||||
return MG_Config::Features.UseAngle;
|
return MG_Config::Features.EsprytUseAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
|
||||||
@@ -1724,13 +1724,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
|
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.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
|
||||||
caps.IsAngleLlvmpipeRenderer =
|
caps.IsAngleLlvmpipeRenderer =
|
||||||
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
||||||
caps.AvoidSamplerMipmapMinFilter =
|
caps.AvoidSamplerMipmapMinFilter =
|
||||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidSamplerMipmapMinFilter;
|
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.EsprytAvoidSamplerMipmapMinFilter;
|
||||||
caps.AvoidExplicitLodBias =
|
caps.AvoidExplicitLodBias =
|
||||||
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidExplicitLodBias;
|
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.EsprytAvoidExplicitLodBias;
|
||||||
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
|
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
|
||||||
caps.SupportsDisjointTimerQuery ? "true" : "false");
|
caps.SupportsDisjointTimerQuery ? "true" : "false");
|
||||||
MGLOG_I(" GL_KHR_parallel_shader_compile supported: %s",
|
MGLOG_I(" GL_KHR_parallel_shader_compile supported: %s",
|
||||||
|
|||||||
@@ -1235,10 +1235,10 @@ namespace MobileGL {
|
|||||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
Bool IsAngleLlvmpipeRenderer = false;
|
Bool IsAngleLlvmpipeRenderer = false;
|
||||||
// IsAngleLlvmpipeRenderer combined with the
|
// 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.
|
// sampler min filters should drop their mipmap component.
|
||||||
Bool AvoidSamplerMipmapMinFilter = false;
|
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.
|
// feature toggle: LOD-bias emulation should not touch explicit-LOD lookups.
|
||||||
Bool AvoidExplicitLodBias = false;
|
Bool AvoidExplicitLodBias = false;
|
||||||
// True when indirect draws leak the command's baseInstance word ("reserved,
|
// True when indirect draws leak the command's baseInstance word ("reserved,
|
||||||
@@ -1247,6 +1247,17 @@ namespace MobileGL {
|
|||||||
// straight to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V
|
// straight to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V
|
||||||
// InstanceIndex, which includes firstInstance.
|
// InstanceIndex, which includes firstInstance.
|
||||||
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
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;
|
Int UniformBufferOffsetAlignment = 256;
|
||||||
// Its storage-buffer counterpart, queried separately because it is a separate limit:
|
// 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
|
// Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool IsShaderSubgroupForcedDisabled() {
|
Bool IsShaderSubgroupForcedDisabled() {
|
||||||
return MG_Config::Features.DisableSubgroup;
|
return MG_Config::Features.MagmaDisableSubgroup;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
supportsShaderSubgroup ? "true" : "false", caps.SupportsShaderSubgroup ? "true" : "false",
|
supportsShaderSubgroup ? "true" : "false", caps.SupportsShaderSubgroup ? "true" : "false",
|
||||||
subgroupProps.subgroupSize, subgroupProps.supportedStages, subgroupProps.supportedOperations);
|
subgroupProps.subgroupSize, subgroupProps.supportedStages, subgroupProps.supportedOperations);
|
||||||
if (supportsShaderSubgroup && forceDisableShaderSubgroup) {
|
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;
|
return true;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
|
|
||||||
#include "DriverBugProbes.h"
|
#include "DriverBugProbes.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -126,6 +128,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
GLint packAlignment = 4;
|
GLint packAlignment = 4;
|
||||||
GLint packRowLength = 0;
|
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 imageName = 0;
|
||||||
GLint imageLevel = 0;
|
GLint imageLevel = 0;
|
||||||
GLint imageLayered = 0;
|
GLint imageLayered = 0;
|
||||||
@@ -165,6 +182,16 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
|
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
|
||||||
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
|
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
|
||||||
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
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) {
|
if (gl.glGetFloatv != nullptr) {
|
||||||
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor);
|
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor);
|
||||||
}
|
}
|
||||||
@@ -220,6 +247,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
if (gl.glPixelStorei != nullptr) {
|
if (gl.glPixelStorei != nullptr) {
|
||||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment);
|
gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment);
|
||||||
gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength);
|
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) {
|
if (gl.glClearColor != nullptr) {
|
||||||
gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
|
gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
|
||||||
@@ -1686,6 +1725,519 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
return measurement;
|
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 {
|
namespace {
|
||||||
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
|
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
|
||||||
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
|
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
|
||||||
@@ -1808,6 +2360,91 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
percentOf(measurement.emittedShapeMismatchedTexels))};
|
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};
|
||||||
|
}
|
||||||
|
|
||||||
// The table. One row per known driver bug; see the header for how to add a sibling.
|
// The table. One row per known driver bug; see the header for how to add a sibling.
|
||||||
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
||||||
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
||||||
@@ -1818,6 +2455,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
&ProbeImageCoherencyResidualBug,
|
&ProbeImageCoherencyResidualBug,
|
||||||
&ProbeExplicitVertexInputLocationCeilingBug,
|
&ProbeExplicitVertexInputLocationCeilingBug,
|
||||||
&ProbeLayeredBlitDestinationBug,
|
&ProbeLayeredBlitDestinationBug,
|
||||||
|
&ProbeLocatedIoBlockPayloadBug,
|
||||||
|
&ProbeCopyImagePacked16FieldOrderBug,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,44 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
String detail;
|
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
|
// 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
|
// copy landed where it was asked to. Returns true only when the destination layer is ignored
|
||||||
// while the control lands correctly.
|
// while the control lands correctly.
|
||||||
@@ -261,6 +299,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
||||||
const MG_External::GLESFunctionsTable& gl);
|
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,
|
// 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".
|
// so an unaffected device renders an empty section rather than a wall of "not affected".
|
||||||
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|||||||
@@ -1557,7 +1557,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
|
|
||||||
// Native iterationRP compute witness. This deliberately uses a separate
|
// Native iterationRP compute witness. This deliberately uses a separate
|
||||||
// throwaway Vulkan device rather than the real renderer's queues, and it
|
// 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.
|
// driver does, not what MobileGL elects to advertise to applications.
|
||||||
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||||
VkInstance instance, VkPhysicalDevice physicalDevice,
|
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||||
|
#include "SpirvPasses/StripIoBlockLocationsPass.h"
|
||||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||||
@@ -1227,6 +1228,31 @@ namespace MobileGL {
|
|||||||
outputBinary, true, enableSpirvValidation);
|
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,
|
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
const bool enableSpirvValidation) {
|
const bool enableSpirvValidation) {
|
||||||
|
|||||||
@@ -154,6 +154,20 @@ namespace MobileGL {
|
|||||||
std::set<String>& renamedBlockNames,
|
std::set<String>& renamedBlockNames,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
bool enableSpirvValidation = false);
|
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
|
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.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 "StripIoBlockLocationsPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
|
||||||
|
// Every struct type carrying the Block decoration, minus the ones with a builtin
|
||||||
|
// member (gl_PerVertex and friends): those are spelled by the language, carry no
|
||||||
|
// user Location, and are not what this pass is about. Same shape as
|
||||||
|
// UniquifyIoBlockNamesPass::CollectUserBlockStructIds, and deliberately kept
|
||||||
|
// beside its own pass rather than shared - the two ask the same question of the
|
||||||
|
// module but are armed by different gates, and one growing a special case must
|
||||||
|
// not silently move the other.
|
||||||
|
std::unordered_set<uint32_t> CollectUserBlockStructIds(IRContext* irContext) {
|
||||||
|
std::unordered_set<uint32_t> blockStructIds;
|
||||||
|
std::unordered_set<uint32_t> builtinStructIds;
|
||||||
|
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::Block) {
|
||||||
|
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
builtinStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const uint32_t builtinStructId : builtinStructIds) {
|
||||||
|
blockStructIds.erase(builtinStructId);
|
||||||
|
}
|
||||||
|
return blockStructIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The interface-block struct an Input/Output variable declares, or 0 when the
|
||||||
|
// variable is not one. Tessellation and geometry interfaces are arrays of the
|
||||||
|
// block struct, so array levels are unwrapped before the struct is recognised.
|
||||||
|
uint32_t GetInterfaceBlockStructId(IRContext* irContext, Instruction& variable,
|
||||||
|
const std::unordered_set<uint32_t>& blockStructIds,
|
||||||
|
spv::StorageClass& outStorageClass) {
|
||||||
|
if (variable.opcode() != spv::Op::OpVariable) return 0;
|
||||||
|
const auto storageClass =
|
||||||
|
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
|
||||||
|
if (storageClass != spv::StorageClass::Input &&
|
||||||
|
storageClass != spv::StorageClass::Output) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||||
|
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||||
|
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
|
||||||
|
Instruction* pointee = defUseMgr->GetDef(pointeeId);
|
||||||
|
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
|
||||||
|
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||||
|
pointeeId = pointee->GetSingleWordInOperand(0);
|
||||||
|
pointee = defUseMgr->GetDef(pointeeId);
|
||||||
|
}
|
||||||
|
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) return 0;
|
||||||
|
if (blockStructIds.find(pointeeId) == blockStructIds.end()) return 0;
|
||||||
|
|
||||||
|
outStorageClass = storageClass;
|
||||||
|
return pointeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool DirectionIsArmed(spv::StorageClass storageClass, Bool stripInputBlocks,
|
||||||
|
Bool stripOutputBlocks) {
|
||||||
|
return storageClass == spv::StorageClass::Input ? stripInputBlocks : stripOutputBlocks;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status StripIoBlockLocationsPass::Process() {
|
||||||
|
if (m_strippedAny != nullptr) *m_strippedAny = false;
|
||||||
|
if (!m_stripInputBlocks && !m_stripOutputBlocks) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
auto* irContext = context();
|
||||||
|
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
|
||||||
|
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// What to strip, resolved BEFORE anything is killed: the walk below deletes
|
||||||
|
// annotations, and deciding what to delete while deleting reads a list that is
|
||||||
|
// being mutated underneath it.
|
||||||
|
//
|
||||||
|
// BOTH LEVELS, because a block carries its location at exactly one of them and
|
||||||
|
// which one is not the caller's choice. When the location came from the
|
||||||
|
// cross-stage IO resolver (or from `layout(location=) out Blk {...}`) glslang
|
||||||
|
// puts it on the VARIABLE; when the application located the members instead
|
||||||
|
// (`out Blk { layout(location = 4) vec4 v; }`) it puts one OpMemberDecorate per
|
||||||
|
// member and NOTHING on the variable - and SPIRV-Cross then suppresses the
|
||||||
|
// block-level qualifier and prints the member ones instead
|
||||||
|
// (spirv_glsl.cpp:1444 and :2037-2045). Stripping only the variable level would
|
||||||
|
// leave that second shape emitting exactly the located block this driver drops
|
||||||
|
// the payload for, and - because there was no variable decoration to remove -
|
||||||
|
// would report nothing stripped, so the caller would decline the module and
|
||||||
|
// nothing would say the repair had passed the shader by.
|
||||||
|
std::unordered_set<uint32_t> armedVariableIds;
|
||||||
|
std::unordered_set<uint32_t> armedStructIds;
|
||||||
|
// Block structs reached by an interface variable whose direction is NOT armed.
|
||||||
|
// A struct in here is left alone even if some armed variable also reaches it:
|
||||||
|
// member decorations belong to the TYPE, so stripping them would take the
|
||||||
|
// qualifier off the unarmed side too - the one whose other end is in a
|
||||||
|
// different program and is matched by exactly that number.
|
||||||
|
std::unordered_set<uint32_t> unarmedStructIds;
|
||||||
|
for (Instruction& variable : irContext->module()->types_values()) {
|
||||||
|
spv::StorageClass storageClass = spv::StorageClass::Input;
|
||||||
|
const uint32_t structId =
|
||||||
|
GetInterfaceBlockStructId(irContext, variable, blockStructIds, storageClass);
|
||||||
|
if (structId == 0) continue;
|
||||||
|
if (DirectionIsArmed(storageClass, m_stripInputBlocks, m_stripOutputBlocks)) {
|
||||||
|
armedVariableIds.insert(variable.result_id());
|
||||||
|
armedStructIds.insert(structId);
|
||||||
|
} else {
|
||||||
|
unarmedStructIds.insert(structId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const uint32_t unarmedStructId : unarmedStructIds) {
|
||||||
|
armedStructIds.erase(unarmedStructId);
|
||||||
|
}
|
||||||
|
if (armedVariableIds.empty()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// Component travels with Location and is meaningless without it. Leaving one
|
||||||
|
// behind is not merely untidy: for an ES target SPIRV-Cross THROWS on a block
|
||||||
|
// member's Component (spirv_glsl.cpp:1447-1460) rather than printing it, which
|
||||||
|
// costs the whole stage.
|
||||||
|
const auto isLocationOrComponent = [](uint32_t decoration) {
|
||||||
|
return static_cast<spv::Decoration>(decoration) == spv::Decoration::Location ||
|
||||||
|
static_cast<spv::Decoration>(decoration) == spv::Decoration::Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Instruction*> toKill;
|
||||||
|
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||||
|
if (!isLocationOrComponent(annotation.GetSingleWordInOperand(1))) continue;
|
||||||
|
if (armedVariableIds.find(annotation.GetSingleWordInOperand(0)) ==
|
||||||
|
armedVariableIds.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toKill.push_back(&annotation);
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||||
|
// OpMemberDecorate <struct> <member> <decoration> ...
|
||||||
|
if (!isLocationOrComponent(annotation.GetSingleWordInOperand(2))) continue;
|
||||||
|
if (armedStructIds.find(annotation.GetSingleWordInOperand(0)) ==
|
||||||
|
armedStructIds.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toKill.push_back(&annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Instruction* inst : toKill) {
|
||||||
|
irContext->KillInst(inst);
|
||||||
|
}
|
||||||
|
if (m_strippedAny != nullptr) *m_strippedAny = !toKill.empty();
|
||||||
|
return toKill.empty() ? Status::SuccessWithoutChange : Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken StripIoBlockLocationsPass::CreateStripIoBlockLocationsPass(
|
||||||
|
Bool stripInputBlocks, Bool stripOutputBlocks, Bool* strippedAny) {
|
||||||
|
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<StripIoBlockLocationsPass>(
|
||||||
|
stripInputBlocks, stripOutputBlocks, strippedAny));
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Drops the Location (and Component) decoration from an inter-stage interface
|
||||||
|
// BLOCK variable, so SPIRV-Cross emits `out FOO { ... } x;` instead of
|
||||||
|
// `layout(location = N) out FOO { ... } x;`.
|
||||||
|
//
|
||||||
|
// WHY. On the Mali-G1-Ultra ES driver (r54p1), an interface block that carries an
|
||||||
|
// explicit layout(location=) transports NOTHING across any boundary that involves
|
||||||
|
// a tessellation or geometry stage. The stages compile, the program links with an
|
||||||
|
// empty info log, the draw runs - and the consuming stage reads zeroes. The same
|
||||||
|
// program with the qualifier removed from the blocks (and nothing else changed)
|
||||||
|
// carries the payload correctly. Measured with no MobileGL in the process at all:
|
||||||
|
// a bare EGL/GLES 3.2 program built from the five ESSL stages MobileGL emits for
|
||||||
|
// KHR-GLxx.shading_language_420pack.length_of_vector_and_matrix_* reproduces it,
|
||||||
|
// and a three-stage VS->GS->FS reduction isolates it to
|
||||||
|
// (block carries a location) AND (a tessellation or geometry stage is present).
|
||||||
|
// A located block between a vertex and a fragment stage is fine on the same
|
||||||
|
// driver, which is why the caller only arms this for programs that have one of
|
||||||
|
// those stages.
|
||||||
|
//
|
||||||
|
// The locations are not the application's: these blocks carry no location in the
|
||||||
|
// GLSL source at all (the 420pack cases declare none). glslang's cross-stage IO
|
||||||
|
// resolver invents them, SPIRV-Cross prints them because ESSL >= 310 allows a
|
||||||
|
// location on a block, and nothing downstream needs them - ES matches inter-stage
|
||||||
|
// blocks by block name plus member sequence, which is exactly what
|
||||||
|
// UniquifyIoBlockNamesPass keeps consistent across the program.
|
||||||
|
//
|
||||||
|
// WHAT. Only variables in Input/Output storage whose (array-unwrapped) pointee is
|
||||||
|
// a Block-decorated struct. Plain varyings keep their locations - they work on
|
||||||
|
// this driver and are how the fragment stage's inputs and outputs are matched -
|
||||||
|
// and so do vertex attributes and fragment outputs, which are never blocks.
|
||||||
|
// Builtin blocks (gl_PerVertex) are skipped; they carry no Location anyway.
|
||||||
|
//
|
||||||
|
// BOTH DECORATION LEVELS, because a block carries its location at exactly one of
|
||||||
|
// them: on the VARIABLE when the cross-stage IO resolver assigned it (or the
|
||||||
|
// application wrote `layout(location=) out Blk {...}`), and on the MEMBERS when the
|
||||||
|
// application located those instead - in which case glslang puts nothing on the
|
||||||
|
// variable at all and SPIRV-Cross suppresses the block-level qualifier in favour of
|
||||||
|
// the member ones. A variable-only strip would silently pass that second shape by.
|
||||||
|
// A struct reached by an interface variable whose direction is NOT armed keeps its
|
||||||
|
// member decorations: they belong to the type, and taking them off would strip the
|
||||||
|
// unarmed side too.
|
||||||
|
//
|
||||||
|
// The two directions are armed SEPARATELY by the caller, because an interface
|
||||||
|
// whose other end lives in a DIFFERENT program (a separable program pipeline)
|
||||||
|
// must keep its location: that is the only thing matching it there, and the other
|
||||||
|
// program never saw this decision. In a monolithic program both ends are present
|
||||||
|
// and both flags are set.
|
||||||
|
//
|
||||||
|
// DirectGLES only: DirectVulkan hands the module to the driver as SPIR-V, where
|
||||||
|
// Location is how interfaces are matched and removing it would be a miscompile.
|
||||||
|
class StripIoBlockLocationsPass final : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
// `stripInputBlocks` covers the blocks this stage CONSUMES and
|
||||||
|
// `stripOutputBlocks` the ones it PRODUCES. `strippedAny`, when non-null,
|
||||||
|
// receives whether this stage actually had one, so the caller can decline the
|
||||||
|
// re-serialised module when there was nothing to strip.
|
||||||
|
StripIoBlockLocationsPass(Bool stripInputBlocks, Bool stripOutputBlocks,
|
||||||
|
Bool* strippedAny = nullptr)
|
||||||
|
: m_stripInputBlocks(stripInputBlocks), m_stripOutputBlocks(stripOutputBlocks),
|
||||||
|
m_strippedAny(strippedAny) {}
|
||||||
|
|
||||||
|
const char* name() const override { return "mobilegl-strip-io-block-locations"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateStripIoBlockLocationsPass(
|
||||||
|
Bool stripInputBlocks, Bool stripOutputBlocks, Bool* strippedAny);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Bool m_stripInputBlocks = false;
|
||||||
|
Bool m_stripOutputBlocks = false;
|
||||||
|
Bool* m_strippedAny = nullptr;
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -194,6 +194,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
static const std::map<String, String> kEmptyRenames;
|
static const std::map<String, String> kEmptyRenames;
|
||||||
builder.StringMap(inputs.inputBlockRenames ? *inputs.inputBlockRenames : kEmptyRenames);
|
builder.StringMap(inputs.inputBlockRenames ? *inputs.inputBlockRenames : kEmptyRenames);
|
||||||
builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames);
|
builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames);
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.stripInputBlockLocations));
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.stripOutputBlockLocations));
|
||||||
static const Vector<Uint32> kEmptyWords;
|
static const Vector<Uint32> kEmptyWords;
|
||||||
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
|
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
|
||||||
return MakeTranslationCacheKey(builder);
|
return MakeTranslationCacheKey(builder);
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// --- driver capability bits that arm or steer a pass ---
|
// --- driver capability bits that arm or steer a pass ---
|
||||||
// Whether LowerViewportIndexForEssl runs on this module. NOT the raw
|
// Whether LowerViewportIndexForEssl runs on this module. NOT the raw
|
||||||
// GL_OES_viewport_array bit any more: the routing emulation arms the pass even where the
|
// GL_OES_viewport_array bit any more: the routing emulation arms the pass even where the
|
||||||
// extension exists (Config.h, ViewportArrayEmulation), so the extension alone no longer
|
// extension exists (Config.h, EsprytViewportArrayEmulation), so the extension alone no longer
|
||||||
// decides, and a key carrying only it would serve a module lowered under one setting to a
|
// decides, and a key carrying only it would serve a module lowered under one setting to a
|
||||||
// link made under the other.
|
// link made under the other.
|
||||||
Bool viewportIndexLoweringArmed = false;
|
Bool viewportIndexLoweringArmed = false;
|
||||||
@@ -658,6 +658,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// arguments, so they are exactly as fine as its behaviour and no finer.
|
// arguments, so they are exactly as fine as its behaviour and no finer.
|
||||||
const std::map<String, String>* inputBlockRenames = nullptr;
|
const std::map<String, String>* inputBlockRenames = nullptr;
|
||||||
const std::map<String, String>* outputBlockRenames = nullptr;
|
const std::map<String, String>* outputBlockRenames = nullptr;
|
||||||
|
// THIS STAGE's share of the interface-block LOCATION strip - the two arguments
|
||||||
|
// StripIoBlockLocationsForEssl is called with, which decide whether the emitted ESSL
|
||||||
|
// prints `layout(location = N)` on a block at all. False for every program on a
|
||||||
|
// driver whose POST said located blocks work, and for every program without a
|
||||||
|
// tessellation or geometry stage. Armed per direction because an interface whose
|
||||||
|
// other end is in a different program must keep its location.
|
||||||
|
Bool stripInputBlockLocations = false;
|
||||||
|
Bool stripOutputBlockLocations = false;
|
||||||
|
|
||||||
// The top of the reserved storage-block window atomic-counter blocks are moved into
|
// The top of the reserved storage-block window atomic-counter blocks are moved into
|
||||||
// (`top - N` for GL binding N). Derived from the driver's
|
// (`top - N` for GL binding N). Derived from the driver's
|
||||||
|
|||||||
@@ -340,17 +340,39 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
// (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats
|
// (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats
|
||||||
// to R8G8B8A8_UNORM), so the two backends now agree here.
|
// to R8G8B8A8_UNORM), so the two backends now agree here.
|
||||||
//
|
//
|
||||||
// Only the DESKTOP-ONLY formats move. GL_RGBA4 and GL_RGB5_A1 are ES formats an
|
// Only the DESKTOP-ONLY formats move UNCONDITIONALLY. GL_RGBA4, GL_RGB5_A1 and
|
||||||
// application can legitimately ask for - the same normalization picks the storage for
|
// GL_RGB565 are ES formats an application can legitimately ask for - the same
|
||||||
// glRenderbufferStorage - so widening them would be a memory decision, not a
|
// normalization picks the storage for glRenderbufferStorage - so widening them
|
||||||
// correctness one. Nothing about the REPORTED precision moves either way:
|
// used to be declined as "a memory decision, not a correctness one". The 18
|
||||||
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on the
|
// KHR-GL4x.copy_image.functional bodies on Mali falsified that: the driver
|
||||||
// requested format, not on the ES storage.
|
// stores SOME packed16 allocations with a MIRRORED field order (allocation-scoped,
|
||||||
|
// shape- and context-dependent; the failing 30x30x12 arrays are mirrored at every
|
||||||
|
// level), so a raw glCopyImageSubData between a mirrored allocation and a plain
|
||||||
|
// one delivers the channels reversed (0x0007 -> 0x3800 for a 5551 word: the
|
||||||
|
// 1_5_5_5_REV re-encoding of the same fields). Where that is measured -
|
||||||
|
// WidenPacked16Norm, set from the POST probe or its ForceOn override - the three
|
||||||
|
// formats take the same 8-bit widening; everywhere else they stay narrow and the
|
||||||
|
// memory argument stands. Nothing about the REPORTED precision moves either way:
|
||||||
|
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on
|
||||||
|
// the requested format, not on the ES storage.
|
||||||
case GL_R3_G3_B2:
|
case GL_R3_G3_B2:
|
||||||
case GL_RGB4:
|
case GL_RGB4:
|
||||||
case GL_RGB5:
|
case GL_RGB5:
|
||||||
*outInternalFormat = GL_RGB8;
|
*outInternalFormat = GL_RGB8;
|
||||||
break;
|
break;
|
||||||
|
// GL_RGB5 above is nominally the same resolution, but a TEXTURE never arrives here
|
||||||
|
// as GL_RGB5: ConvertGLEnumToTextureInternalFormat folds GL_RGB5 and GL_RGB565 onto
|
||||||
|
// one logical format whose GL spelling is GL_RGB565, so this case is the one the
|
||||||
|
// allocation path actually reaches for both spellings.
|
||||||
|
case GL_RGB565:
|
||||||
|
*outInternalFormat =
|
||||||
|
(options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGB8 : internalFormat;
|
||||||
|
break;
|
||||||
|
case GL_RGB5_A1:
|
||||||
|
case GL_RGBA4:
|
||||||
|
*outInternalFormat =
|
||||||
|
(options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGBA8 : internalFormat;
|
||||||
|
break;
|
||||||
case GL_RGB10:
|
case GL_RGB10:
|
||||||
case GL_RGB12:
|
case GL_RGB12:
|
||||||
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ namespace MobileGL {
|
|||||||
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
||||||
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
||||||
NoSnorm8RenderTarget = 1 << 9,
|
NoSnorm8RenderTarget = 1 << 9,
|
||||||
|
// Store the three 16-bit packed normalized formats (GL_RGB565, GL_RGB5_A1, GL_RGBA4)
|
||||||
|
// as 8-bit-per-channel ES storage (GL_RGB8 / GL_RGBA8), the way the desktop-only
|
||||||
|
// narrow formats already are. Set by DirectGLES when the driver's 16-bit packed
|
||||||
|
// storage cannot be trusted as a raw-copy endpoint: some Mali drivers store SOME
|
||||||
|
// packed16 allocations with a MIRRORED field order (which ones depends on shape and
|
||||||
|
// context history - measured on a three-level 30x30x12 2D array, every level of it),
|
||||||
|
// so glCopyImageSubData (a raw texel-block move) between a mirrored allocation and a
|
||||||
|
// plain one delivers the channels reversed. The
|
||||||
|
// (format, type) transfer pair does not move with the bit - it is already the
|
||||||
|
// UNorm8 component layout the canonical shadow holds for all three formats.
|
||||||
|
// Reported precision does not move either: GL_TEXTURE_*_SIZE and
|
||||||
|
// glGetInternalformativ answer from TextureMetrics, keyed on the requested format.
|
||||||
|
WidenPacked16Norm = 1 << 10,
|
||||||
None = 0,
|
None = 0,
|
||||||
};
|
};
|
||||||
namespace MG_Util::TextureFormatProcessor {
|
namespace MG_Util::TextureFormatProcessor {
|
||||||
|
|||||||
@@ -157,12 +157,12 @@ MobileGL supports runtime configuration via environment variables.
|
|||||||
|-------------------------|--------------------------------------------------|--------------------------------------|----------------|
|
|-------------------------|--------------------------------------------------|--------------------------------------|----------------|
|
||||||
| `MOBILEGL_BACKEND_TYPE` | Select active backend implementation at startup. | `DirectGLES`, `DirectVulkan` | `DirectGLES` |
|
| `MOBILEGL_BACKEND_TYPE` | Select active backend implementation at startup. | `DirectGLES`, `DirectVulkan` | `DirectGLES` |
|
||||||
| `MOBILEGL_DISABLE_TIMERQUERY` | Disable GPU timer-query exposure and use. | `0`, `1` | `0` |
|
| `MOBILEGL_DISABLE_TIMERQUERY` | Disable GPU timer-query exposure and use. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_USE_ANGLE` | Load ANGLE EGL/GLES libraries. | `0`, `1` | `0` |
|
| `MOBILEGL_ESPRYT_USE_ANGLE` | Load ANGLE EGL/GLES libraries. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
|
| `MOBILEGL_MAGMA_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
|
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
|
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
|
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
|
||||||
| `MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
|
| `MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_COHERENT_AS_FLUSH` | Treat persistent `GL_MAP_FLUSH_EXPLICIT_BIT` maps as coherent (app-compat for engines like Flywheel that never flush them). | `0`, `1` | `0` |
|
| `MOBILEGL_COHERENT_AS_FLUSH` | Treat persistent `GL_MAP_FLUSH_EXPLICIT_BIT` maps as coherent (app-compat for engines like Flywheel that never flush them). | `0`, `1` | `0` |
|
||||||
| `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION` | Always emulate depth/stencil `glReadPixels`/`glGetTexImage` by shader sampling on Espryt, instead of using the driver's own depth/stencil readback where it has one. | `0`, `1` | `0` |
|
| `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION` | Always emulate depth/stencil `glReadPixels`/`glGetTexImage` by shader sampling on Espryt, instead of using the driver's own depth/stencil readback where it has one. | `0`, `1` | `0` |
|
||||||
| `VK_ICD_FILENAMES` | Select the Vulkan ICD used by the Vulkan loader. | Path to an ICD JSON file | Loader default |
|
| `VK_ICD_FILENAMES` | Select the Vulkan ICD used by the Vulkan loader. | Path to an ICD JSON file | Loader default |
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Implementation notes:
|
|||||||
- `DirectGLES` and `DirectVulkan` replay on the Activity `SurfaceView` by default. DirectGLES can still use the old offscreen EGL pbuffer path by passing `use_pbuffer=true`.
|
- `DirectGLES` and `DirectVulkan` replay on the Activity `SurfaceView` by default. DirectGLES can still use the old offscreen EGL pbuffer path by passing `use_pbuffer=true`.
|
||||||
- Golden comparison is implemented in native C++ with libpng RGBA decode and SSIM validation. The Java Activity only passes arguments and displays the native result, so the replay/compare core is not tied to Android UI or Bitmap APIs and can be ported to Linux.
|
- Golden comparison is implemented in native C++ with libpng RGBA decode and SSIM validation. The Java Activity only passes arguments and displays the native result, so the replay/compare core is not tied to Android UI or Bitmap APIs and can be ported to Linux.
|
||||||
- The plugin profile still excludes `libtrace_replay_runner.so`; normal plugin APK behavior is preserved.
|
- The plugin profile still excludes `libtrace_replay_runner.so`; normal plugin APK behavior is preserved.
|
||||||
- Set `MOBILEGL_USE_ANGLE=1` and `MOBILEGL_TRACE_ANGLE_VARIANT=<short-hash>` when running `trace-replay-ci.sh` for DirectGLES. The trace APK contains both allowlisted ANGLE builds with short-hash filenames and SONAMEs; MobileGL resolves its signed native library directory and loads the selected pair by absolute path. Set `MOBILEGL_RETRACE_USE_PBUFFER=1` or pass `--use-pbuffer` to keep DirectGLES offscreen.
|
- Set `MOBILEGL_ESPRYT_USE_ANGLE=1` and `MOBILEGL_TRACE_ANGLE_VARIANT=<short-hash>` when running `trace-replay-ci.sh` for DirectGLES. The trace APK contains both allowlisted ANGLE builds with short-hash filenames and SONAMEs; MobileGL resolves its signed native library directory and loads the selected pair by absolute path. Set `MOBILEGL_RETRACE_USE_PBUFFER=1` or pass `--use-pbuffer` to keep DirectGLES offscreen.
|
||||||
|
|
||||||
Example core-profile trace smoke command for a debug trace APK:
|
Example core-profile trace smoke command for a debug trace APK:
|
||||||
|
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ val pluginRendererConfig = buildJsonValue {
|
|||||||
items = RendererConfig.EnvItems("DirectGLES", listOf("DirectVulkan")),
|
items = RendererConfig.EnvItems("DirectGLES", listOf("DirectVulkan")),
|
||||||
)
|
)
|
||||||
toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title"))
|
toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title"))
|
||||||
toggleable("MOBILEGL_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title"))
|
toggleable("MOBILEGL_MAGMA_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title"))
|
||||||
toggleable("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", false, RendererConfig.MetaString("mobilegl_magma_r11g11b10f_fallback_title"))
|
toggleable("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", false, RendererConfig.MetaString("mobilegl_magma_r11g11b10f_fallback_title"))
|
||||||
customizable("MOBILEGL_MAGMA_FRAMESINFLIGHT", "3", RendererConfig.MetaString("mobilegl_magma_frames_inflight_title"))
|
customizable("MOBILEGL_MAGMA_FRAMESINFLIGHT", "3", RendererConfig.MetaString("mobilegl_magma_frames_inflight_title"))
|
||||||
toggleable("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", false, RendererConfig.MetaString("mobilegl_avoid_sampler_mipmap_min_filter_title"))
|
toggleable("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", false, RendererConfig.MetaString("mobilegl_avoid_sampler_mipmap_min_filter_title"))
|
||||||
toggleable("MOBILEGL_COHERENT_AS_FLUSH", "1", false, RendererConfig.MetaString("mobilegl_coherent_as_flush_title"))
|
toggleable("MOBILEGL_COHERENT_AS_FLUSH", "1", false, RendererConfig.MetaString("mobilegl_coherent_as_flush_title"))
|
||||||
toggleable("MOBILEGL_RELAXED_SEMANTICS", "1", false, RendererConfig.MetaString("mobilegl_relaxed_semantics_title"))
|
toggleable("MOBILEGL_RELAXED_SEMANTICS", "1", false, RendererConfig.MetaString("mobilegl_relaxed_semantics_title"))
|
||||||
toggleable("MOBILEGL_USE_ANGLE", "1", false, RendererConfig.MetaString("mobilegl_use_angle_title"))
|
toggleable("MOBILEGL_ESPRYT_USE_ANGLE", "1", false, RendererConfig.MetaString("mobilegl_use_angle_title"))
|
||||||
},
|
},
|
||||||
minMCVer = null,
|
minMCVer = null,
|
||||||
maxMCVer = null,
|
maxMCVer = null,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ bool UseAngleForRequest(const Request& request) {
|
|||||||
if (request.useAngle) {
|
if (request.useAngle) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const char* value = getenv("MOBILEGL_USE_ANGLE");
|
const char* value = getenv("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||||
return value != nullptr && strcmp(value, "1") == 0;
|
return value != nullptr && strcmp(value, "1") == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,21 +146,21 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
|||||||
unsetenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
unsetenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
}
|
}
|
||||||
if (UseAngleForRequest(request)) {
|
if (UseAngleForRequest(request)) {
|
||||||
setenv("MOBILEGL_USE_ANGLE", "1", 1);
|
setenv("MOBILEGL_ESPRYT_USE_ANGLE", "1", 1);
|
||||||
setenv("MOBILEGL_TRACE_ANGLE_VARIANT", request.angleVariant.c_str(), 1);
|
setenv("MOBILEGL_TRACE_ANGLE_VARIANT", request.angleVariant.c_str(), 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_USE_ANGLE");
|
unsetenv("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||||
unsetenv("MOBILEGL_TRACE_ANGLE_VARIANT");
|
unsetenv("MOBILEGL_TRACE_ANGLE_VARIANT");
|
||||||
}
|
}
|
||||||
if (request.avoidAngleLlvmpipeSamplerMipmapMinFilter) {
|
if (request.avoidAngleLlvmpipeSamplerMipmapMinFilter) {
|
||||||
setenv("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", 1);
|
setenv("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
unsetenv("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
}
|
}
|
||||||
if (request.avoidAngleLlvmpipeExplicitLodBias) {
|
if (request.avoidAngleLlvmpipeExplicitLodBias) {
|
||||||
setenv("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS", "1", 1);
|
setenv("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS", "1", 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
unsetenv("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||||
}
|
}
|
||||||
if (request.coherentAsFlush) {
|
if (request.coherentAsFlush) {
|
||||||
setenv("MOBILEGL_COHERENT_AS_FLUSH", "1", 1);
|
setenv("MOBILEGL_COHERENT_AS_FLUSH", "1", 1);
|
||||||
@@ -168,19 +168,19 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
|||||||
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
|
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
}
|
}
|
||||||
if (request.fixIterationRPSubgroupScratch) {
|
if (request.fixIterationRPSubgroupScratch) {
|
||||||
setenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
|
setenv("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
unsetenv("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
}
|
}
|
||||||
if (request.deriveNumSubgroups) {
|
if (request.deriveNumSubgroups) {
|
||||||
setenv("MOBILEGL_DERIVE_NUM_SUBGROUPS", "1", 1);
|
setenv("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS", "1", 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
unsetenv("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||||
}
|
}
|
||||||
if (request.iterationRPFixBarrier) {
|
if (request.iterationRPFixBarrier) {
|
||||||
setenv("MOBILEGL_ITERATIONRP_FIX_BARRIER", "1", 1);
|
setenv("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER", "1", 1);
|
||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
unsetenv("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||||
}
|
}
|
||||||
if (request.fboAttachmentDumps.empty()) {
|
if (request.fboAttachmentDumps.empty()) {
|
||||||
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
|
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
|
||||||
|
|||||||
@@ -38,20 +38,20 @@ Usage:
|
|||||||
[--reuse-fixture] \
|
[--reuse-fixture] \
|
||||||
--timeout-seconds N
|
--timeout-seconds N
|
||||||
|
|
||||||
Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE
|
Set MOBILEGL_ESPRYT_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE
|
||||||
instead of the device system GLES driver.
|
instead of the device system GLES driver.
|
||||||
Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
|
Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
|
||||||
DirectGLES replay.
|
DirectGLES replay.
|
||||||
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
|
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
|
||||||
against an offscreen EGL pbuffer instead of the Activity surface.
|
against an offscreen EGL pbuffer instead of the Activity surface.
|
||||||
Set MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
|
Set MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
|
||||||
MOBILEGL_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_ITERATIONRP_FIX_BARRIER=1 to
|
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1 to
|
||||||
forward the corresponding iterationRP SPIR-V repairs into the APK process.
|
forward the corresponding iterationRP SPIR-V repairs into the APK process.
|
||||||
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
|
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
|
||||||
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
|
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
|
||||||
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
|
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
|
||||||
sample with an explicit LOD that ANGLE llvmpipe cannot take a LOD bias on
|
sample with an explicit LOD that ANGLE llvmpipe cannot take a LOD bias on
|
||||||
(MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1).
|
(MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS=1).
|
||||||
Pass --coherent-as-flush for traces whose engine writes persistent
|
Pass --coherent-as-flush for traces whose engine writes persistent
|
||||||
GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1).
|
GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1).
|
||||||
Pass --benchmark to replay the whole trace as a frame-timing benchmark instead of
|
Pass --benchmark to replay the whole trace as a frame-timing benchmark instead of
|
||||||
@@ -348,7 +348,7 @@ run_retrace() {
|
|||||||
if [ -n "${alternate_golden_path}" ]; then
|
if [ -n "${alternate_golden_path}" ]; then
|
||||||
alternate_golden_app_path="${app_dir}/input/alternate-golden.png"
|
alternate_golden_app_path="${app_dir}/input/alternate-golden.png"
|
||||||
fi
|
fi
|
||||||
if [ "${MOBILEGL_USE_ANGLE:-}" = "1" ] && [ "${backend}" = "DirectGLES" ]; then
|
if [ "${MOBILEGL_ESPRYT_USE_ANGLE:-}" = "1" ] && [ "${backend}" = "DirectGLES" ]; then
|
||||||
use_angle=1
|
use_angle=1
|
||||||
test -n "${MOBILEGL_TRACE_ANGLE_VARIANT:-}" || die "MOBILEGL_TRACE_ANGLE_VARIANT is required for DirectGLES ANGLE replay"
|
test -n "${MOBILEGL_TRACE_ANGLE_VARIANT:-}" || die "MOBILEGL_TRACE_ANGLE_VARIANT is required for DirectGLES ANGLE replay"
|
||||||
fi
|
fi
|
||||||
@@ -388,13 +388,13 @@ run_retrace() {
|
|||||||
if [ "${coherent_as_flush}" -eq 1 ]; then
|
if [ "${coherent_as_flush}" -eq 1 ]; then
|
||||||
set -- "$@" --ez coherent_as_flush true
|
set -- "$@" --ez coherent_as_flush true
|
||||||
fi
|
fi
|
||||||
if [ "${MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
|
if [ "${MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
|
||||||
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
|
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
|
||||||
fi
|
fi
|
||||||
if [ "${MOBILEGL_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
|
if [ "${MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
|
||||||
set -- "$@" --ez derive_num_subgroups true
|
set -- "$@" --ez derive_num_subgroups true
|
||||||
fi
|
fi
|
||||||
if [ "${MOBILEGL_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
|
if [ "${MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
|
||||||
set -- "$@" --ez iterationrp_fix_barrier true
|
set -- "$@" --ez iterationrp_fix_barrier true
|
||||||
fi
|
fi
|
||||||
if [ -n "${texture_2d_dumps}" ]; then
|
if [ -n "${texture_2d_dumps}" ]; then
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ def backend_env(backend, device_dir, use_angle=False):
|
|||||||
'MOBILEGL_LOG_FILE_PATH': f'{device_dir}/mobilegl.log',
|
'MOBILEGL_LOG_FILE_PATH': f'{device_dir}/mobilegl.log',
|
||||||
}
|
}
|
||||||
if backend == 'DirectGLES':
|
if backend == 'DirectGLES':
|
||||||
env['MOBILEGL_USE_ANGLE'] = '1' if use_angle else '0'
|
env['MOBILEGL_ESPRYT_USE_ANGLE'] = '1' if use_angle else '0'
|
||||||
if backend == 'DirectVulkan':
|
if backend == 'DirectVulkan':
|
||||||
# The device ICD usually lacks VK_EXT_headless_surface, which the
|
# The device ICD usually lacks VK_EXT_headless_surface, which the
|
||||||
# MobileGL pbuffer path needs; use a real ANativeWindow from
|
# MobileGL pbuffer path needs; use a real ANativeWindow from
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ unflushed persistent maps, e.g. the Create fixtures), pass
|
|||||||
`MOBILEGL_COHERENT_AS_FLUSH=1`. For cases registered with
|
`MOBILEGL_COHERENT_AS_FLUSH=1`. For cases registered with
|
||||||
`avoid_angle_llvmpipe_explicit_lod_bias` (DirectGLES on ANGLE llvmpipe, e.g. the
|
`avoid_angle_llvmpipe_explicit_lod_bias` (DirectGLES on ANGLE llvmpipe, e.g. the
|
||||||
sundial-lite fixture), pass `--ez avoid_angle_llvmpipe_explicit_lod_bias true` so
|
sundial-lite fixture), pass `--ez avoid_angle_llvmpipe_explicit_lod_bias true` so
|
||||||
the replay runs with `MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1`.
|
the replay runs with `MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS=1`.
|
||||||
|
|
||||||
## Benchmark mode (frame timing)
|
## Benchmark mode (frame timing)
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ ln -sf $ANGLE/libEGL.so libEGL.so
|
|||||||
ln -sf $ANGLE/libGLESv2.so libGLESv2.so
|
ln -sf $ANGLE/libGLESv2.so libGLESv2.so
|
||||||
ln -sf $ANGLE/libvulkan.so.1 libvulkan.so.1 # else eglInitialize fails
|
ln -sf $ANGLE/libvulkan.so.1 libvulkan.so.1 # else eglInitialize fails
|
||||||
|
|
||||||
MOBILEGL_USE_ANGLE=1 \
|
MOBILEGL_ESPRYT_USE_ANGLE=1 \
|
||||||
LD_LIBRARY_PATH=~/angle-farm:/path/to/build/ \
|
LD_LIBRARY_PATH=~/angle-farm:/path/to/build/ \
|
||||||
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json \
|
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json \
|
||||||
ANGLE_DEFAULT_PLATFORM=vulkan \
|
ANGLE_DEFAULT_PLATFORM=vulkan \
|
||||||
|
|||||||
@@ -193,12 +193,12 @@ def run_case(case, backend, extra_args=None, timeout_seconds=None):
|
|||||||
env["PYTHON"] = "python"
|
env["PYTHON"] = "python"
|
||||||
env["MSYS2_ARG_CONV_EXCL"] = "/data/*"
|
env["MSYS2_ARG_CONV_EXCL"] = "/data/*"
|
||||||
if backend_info["use_angle"]:
|
if backend_info["use_angle"]:
|
||||||
env["MOBILEGL_USE_ANGLE"] = "1"
|
env["MOBILEGL_ESPRYT_USE_ANGLE"] = "1"
|
||||||
env["MOBILEGL_TRACE_ANGLE_VARIANT"] = (
|
env["MOBILEGL_TRACE_ANGLE_VARIANT"] = (
|
||||||
BLISS_ANGLE_VARIANT if case["name"] == BLISS_CASE else DEFAULT_ANGLE_VARIANT
|
BLISS_ANGLE_VARIANT if case["name"] == BLISS_CASE else DEFAULT_ANGLE_VARIANT
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
env.pop("MOBILEGL_USE_ANGLE", None)
|
env.pop("MOBILEGL_ESPRYT_USE_ANGLE", None)
|
||||||
env.pop("MOBILEGL_TRACE_ANGLE_VARIANT", None)
|
env.pop("MOBILEGL_TRACE_ANGLE_VARIANT", None)
|
||||||
result = subprocess.run(command, cwd=ROOT, env=env)
|
result = subprocess.run(command, cwd=ROOT, env=env)
|
||||||
copy_goldens(case, backend)
|
copy_goldens(case, backend)
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ uninstall that one package first or the install fails with
|
|||||||
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
|
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
|
||||||
|
|
||||||
Match the CI environment (`.github/workflows/apk.yml` matrix): the emulator
|
Match the CI environment (`.github/workflows/apk.yml` matrix): the emulator
|
||||||
boots with `--gpu software` + `MOBILEGL_USE_ANGLE=1` for `DirectGLES`
|
boots with `--gpu software` + `MOBILEGL_ESPRYT_USE_ANGLE=1` for `DirectGLES`
|
||||||
and `--gpu lavapipe` + `MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1` for
|
and `--gpu lavapipe` + `MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1` for
|
||||||
`DirectVulkan`. The emulator's ANGLE-on-Vulkan GLES stack exercises genuinely
|
`DirectVulkan`. The emulator's ANGLE-on-Vulkan GLES stack exercises genuinely
|
||||||
different driver semantics than physical devices (e.g. indirect-draw
|
different driver semantics than physical devices (e.g. indirect-draw
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ bool MatchOption(const std::string &arg, const char *name, std::string &inlineVa
|
|||||||
|
|
||||||
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
|
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
|
||||||
request.backend = "DirectGLES";
|
request.backend = "DirectGLES";
|
||||||
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||||
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||||
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
const std::string arg = argv[i];
|
const std::string arg = argv[i];
|
||||||
|
|||||||
Reference in New Issue
Block a user