mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
252
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
666f150202 | ||
|
|
2f62970dd5 | ||
|
|
dcb568d445 | ||
|
|
a5f36c8f8d | ||
|
|
1ebe9d11c5 | ||
|
|
a8bb63950d | ||
|
|
7d68a17774 | ||
|
|
415645ccdd | ||
|
|
e6d03eb2a1 | ||
|
|
5e29e7d266 | ||
|
|
7eac33d17b | ||
|
|
8f19ce6fa7 | ||
|
|
d0f7fb99db | ||
|
|
d4247db6c3 | ||
|
|
e4f41e0fd3 | ||
|
|
9cf340cbef | ||
|
|
348a30a816 | ||
|
|
b5e0ada97e | ||
|
|
cb27ac7761 | ||
|
|
38c56a3d38 | ||
|
|
908172ba0f | ||
|
|
e18bac8cb2 | ||
|
|
7b0f443d3a | ||
|
|
f1b4a5e07f | ||
|
|
f3cd4091bf | ||
|
|
529d26f38f | ||
|
|
9bd125aeec | ||
|
|
ece9491d4b | ||
|
|
51b4abd801 | ||
|
|
cbb616093b | ||
|
|
e5846569ca | ||
|
|
194c2f189b | ||
|
|
7de7cfc6eb | ||
|
|
03e69fc9ef | ||
|
|
ee74c8ea3a | ||
|
|
54bbe805e5 | ||
|
|
6e2a3b3496 | ||
|
|
f5a0779385 | ||
|
|
86fdc68efa | ||
|
|
1185265e22 | ||
|
|
77c05b151a | ||
|
|
fdbe0b3117 | ||
|
|
9c9739e1c3 | ||
|
|
87548ae78a | ||
|
|
2a902ff58c | ||
|
|
a79eadd724 | ||
|
|
518e9c7796 | ||
|
|
a4c11f2603 | ||
|
|
37a656dedc | ||
|
|
3cc6b88767 | ||
|
|
b32c35a113 | ||
|
|
8587b83be3 | ||
|
|
0cef345d61 | ||
|
|
31367de628 | ||
|
|
bc4b62026a | ||
|
|
50da7de737 | ||
|
|
d9def5c1bb | ||
|
|
21a4c8aa95 | ||
|
|
6317066add | ||
|
|
02b59bef80 | ||
|
|
668f3e90c9 | ||
|
|
07d6277f87 | ||
|
|
b164692387 | ||
|
|
80ea44573e | ||
|
|
2a7d6f2e16 | ||
|
|
3a12f6d4f3 | ||
|
|
36b9d26b9d | ||
|
|
4b41f01b68 | ||
|
|
e81e938bb8 | ||
|
|
5fa849674e | ||
|
|
aed10f65a6 | ||
|
|
a687fc4577 | ||
|
|
ef66aea73b | ||
|
|
c129cdec2d | ||
|
|
7a7340ebe2 | ||
|
|
325ba07776 | ||
|
|
7aa91e8024 | ||
|
|
1b5a39473e | ||
|
|
0e5f591cfa | ||
|
|
79336c5ccc | ||
|
|
be45dbcf54 | ||
|
|
f7dfa01c18 | ||
|
|
50efa4410a | ||
|
|
bea3086b41 | ||
|
|
8ae93c837d | ||
|
|
4154f2e941 | ||
|
|
cd07d42a47 | ||
|
|
ae0373eb48 | ||
|
|
7480bf4490 | ||
|
|
54b206d90c | ||
|
|
5daf7bf093 | ||
|
|
a8228ca287 | ||
|
|
8b827bd2ce | ||
|
|
6aa161fee7 | ||
|
|
48a70fea81 | ||
|
|
6ea4f32635 | ||
|
|
dc1fffb041 | ||
|
|
d24d5b5ccd | ||
|
|
51883cf1a3 | ||
|
|
6dfadeb7d2 | ||
|
|
4fc3531d0d | ||
|
|
9bde0e500f | ||
|
|
3477d87b50 | ||
|
|
c2a081fa75 | ||
|
|
685fd750c9 | ||
|
|
02c9b8a32d | ||
|
|
26f02567d7 | ||
|
|
a3dbe234d7 | ||
|
|
de8e7a4606 | ||
|
|
31a5da6190 | ||
|
|
8899f065f4 | ||
|
|
a991f63899 | ||
|
|
3ff9cfe5c2 | ||
|
|
6359fba455 | ||
|
|
872876961d | ||
|
|
e2923a239f | ||
|
|
1740a8a41a | ||
|
|
6b1d89f279 | ||
|
|
01fbe0b4b0 | ||
|
|
cb155c5b94 | ||
|
|
04a06438c5 | ||
|
|
db00774224 | ||
|
|
f378c1a064 | ||
|
|
421ccd08c6 | ||
|
|
039af520bf | ||
|
|
cdba7bed2e | ||
|
|
1eeeb44d94 | ||
|
|
fa2e15c27e | ||
|
|
14744f117c | ||
|
|
8329ab4264 | ||
|
|
ee98c453ed | ||
|
|
f88322ce84 | ||
|
|
93f1106ba4 | ||
|
|
31b5b563d6 | ||
|
|
a9fb7ef0af | ||
|
|
6159166d38 | ||
|
|
c6d1b29407 | ||
|
|
5fecfa42f6 | ||
|
|
d48e5d0053 | ||
|
|
0995dfea35 | ||
|
|
7a0182b58f | ||
|
|
0f523db14d | ||
|
|
442cec1a15 | ||
|
|
246a438138 | ||
|
|
042c61fb75 | ||
|
|
a8bebe1a3c | ||
|
|
85cd6913b3 | ||
|
|
afebf38e90 | ||
|
|
898c39f1de | ||
|
|
b1774e80be | ||
|
|
a9b4c47fea | ||
|
|
1c0be3e715 | ||
|
|
27ec3d3438 | ||
|
|
52718ecf84 | ||
|
|
baeb2fa1bc | ||
|
|
54a88ef1e2 | ||
|
|
ee124018a2 | ||
|
|
d7ce0c48ef | ||
|
|
0b3101bf6b | ||
|
|
a4fda520ed | ||
|
|
f0fd6407ae | ||
|
|
bde14cae29 | ||
|
|
f2f6430e34 | ||
|
|
15e36ad1e9 | ||
|
|
7940a09491 | ||
|
|
1e8d4661e6 | ||
|
|
916702629e | ||
|
|
9b37c77ae2 | ||
|
|
085eb5835b | ||
|
|
56377d2025 | ||
|
|
7d2c16a90e | ||
|
|
261cfd1591 | ||
|
|
bbc7b9ca84 | ||
|
|
c1b3b16cab | ||
|
|
f17cb23ea3 | ||
|
|
f297af7d2b | ||
|
|
d9abf1c2c1 | ||
|
|
392736fb6b | ||
|
|
0944925679 | ||
|
|
eadf7bc474 | ||
|
|
00a326ef78 | ||
|
|
c09045fe59 | ||
|
|
5bd8ef01e5 | ||
|
|
3181ed2c5a | ||
|
|
6aed3b08f3 | ||
|
|
281467a345 | ||
|
|
c7e36986e7 | ||
|
|
d8576a2ed3 | ||
|
|
2b6c2b561c | ||
|
|
12c94111b5 | ||
|
|
7769156cfc | ||
|
|
0ecfdff4e7 | ||
|
|
6df5a6137f | ||
|
|
b3794f4e6a | ||
|
|
14d3901d30 | ||
|
|
d4766513e4 | ||
|
|
72dc7aa6aa | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a | ||
|
|
6a80a82dd3 | ||
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 | ||
|
|
bce34d7fac | ||
|
|
f91857266f | ||
|
|
49cb1be0fd | ||
|
|
a51c68bb2c | ||
|
|
1c723a6cfc | ||
|
|
44805bfa07 | ||
|
|
257fcbfd0b | ||
|
|
2b46a3db96 | ||
|
|
0b36621069 | ||
|
|
9ee2e0a1db | ||
|
|
6b6623ae72 | ||
|
|
a6e029734b | ||
|
|
e7d6bfddac | ||
|
|
f2c879528f | ||
|
|
eaba4ac1dc | ||
|
|
ed6578954e | ||
|
|
e005c8b6cb | ||
|
|
535b5e3095 | ||
|
|
1b05a84928 | ||
|
|
7ccb762936 | ||
|
|
442e7eec1c | ||
|
|
2787d15706 | ||
|
|
74ce58a6c7 | ||
|
|
bb122ebd4f | ||
|
|
9b0ed5b3af | ||
|
|
0e31c1481b |
@@ -209,7 +209,7 @@ jobs:
|
|||||||
- name: Load trace cases
|
- name: Load trace cases
|
||||||
id: trace-cases
|
id: trace-cases
|
||||||
run: |
|
run: |
|
||||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
|
||||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
trace-fixtures:
|
trace-fixtures:
|
||||||
@@ -337,13 +337,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||||
backend:
|
|
||||||
- name: DirectGLES
|
|
||||||
gpu: software
|
|
||||||
- name: DirectVulkan
|
|
||||||
gpu: lavapipe
|
|
||||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
uses: pierotofy/set-swap-space@v1.0
|
uses: pierotofy/set-swap-space@v1.0
|
||||||
@@ -426,6 +420,9 @@ jobs:
|
|||||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
MOBILEGL_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_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
|
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
run: |
|
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,6 +265,9 @@ 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_DERIVE_NUM_SUBGROUPS: "1"
|
||||||
|
MOBILEGL_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'
|
||||||
@@ -491,6 +494,7 @@ jobs:
|
|||||||
- benchmark
|
- benchmark
|
||||||
- integration
|
- integration
|
||||||
outputs:
|
outputs:
|
||||||
|
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||||
names: ${{ steps.trace-cases.outputs.names }}
|
names: ${{ steps.trace-cases.outputs.names }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -498,7 +502,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Load trace cases
|
- name: Load trace cases
|
||||||
id: trace-cases
|
id: trace-cases
|
||||||
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
run: |
|
||||||
|
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
trace-fixtures:
|
trace-fixtures:
|
||||||
name: trace fixture (${{ matrix.case }})
|
name: trace fixture (${{ matrix.case }})
|
||||||
@@ -577,11 +583,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
|
||||||
backend:
|
|
||||||
- DirectGLES
|
|
||||||
- DirectVulkan
|
|
||||||
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
@@ -640,6 +642,12 @@ jobs:
|
|||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||||
fi
|
fi
|
||||||
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||||
|
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
||||||
|
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||||
|
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
||||||
|
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
||||||
|
fi
|
||||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
# 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
|
||||||
# bypasses only the vendor gate, so this exercises the real strip on
|
# bypasses only the vendor gate, so this exercises the real strip on
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
|
|||||||
tools/trace_replay/work/
|
tools/trace_replay/work/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
/.gradle
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: 10935bb5e4...c8036190fc
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 6f12598784...fa562bb911
+27
-1
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
|||||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||||
|
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||||
|
|
||||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||||
@@ -269,6 +270,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
@@ -277,24 +279,40 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/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/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
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|
||||||
|
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||||
@@ -378,6 +396,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
@@ -455,7 +474,7 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
# Header-only submodule: no add_subdirectory, no link target. Only
|
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
# pimpl so no consumer target needs this path.
|
# pimpl so no consumer target needs this path.
|
||||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
@@ -667,3 +686,10 @@ if (NOT ANDROID)
|
|||||||
add_subdirectory(tools/trace_replay)
|
add_subdirectory(tools/trace_replay)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The integration binary is also useful as a standalone adb-shell executable.
|
||||||
|
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
||||||
|
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
||||||
|
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||||
|
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||||
|
endif()
|
||||||
|
|||||||
+66
-10
@@ -66,22 +66,53 @@ namespace MobileGL::MG_Config {
|
|||||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||||
// (see MG_Util/Debug/Log.cpp).
|
// (see MG_Util/Debug/Log.cpp).
|
||||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
|
||||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
|
||||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
|
||||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
|
||||||
// SpirvValidationEnabled).
|
|
||||||
struct FeaturesTable {
|
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_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||||
|
// Disabled by default because validation is a diagnostics-only cost.
|
||||||
|
Bool EnableSpirvValidation = false;
|
||||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||||
Bool UseAngle = false;
|
Bool UseAngle = 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_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||||
|
// including the opt-in emulated compute path below.
|
||||||
Bool DisableSubgroup = false;
|
Bool DisableSubgroup = false;
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||||
|
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||||
|
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||||
|
// engages when this flag is set AND the device has no native subgroup support at
|
||||||
|
// all - a device with real subgroup operations always uses them natively,
|
||||||
|
// whatever their width (the known iterationRP defect is patched by
|
||||||
|
// FixIterationRPSubgroupScratch below instead). Off by default.
|
||||||
|
Bool MagmaEmulateSubgroup = false;
|
||||||
|
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||||
|
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||||
|
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||||
|
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||||
|
// bounds. The pass grows that one array to what the device's topology needs and
|
||||||
|
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||||
|
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||||
|
// so every other shader passes through byte-identical - as does iterationRP
|
||||||
|
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||||
|
// verbatim.
|
||||||
|
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||||
|
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||||
|
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||||
|
Bool IterationRPFixBarrier = false;
|
||||||
|
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||||
|
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||||
|
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||||
|
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||||
|
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||||
|
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||||
|
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||||
|
// to the raw driver builtin.
|
||||||
|
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
||||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
// 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
|
||||||
@@ -117,16 +148,19 @@ namespace MobileGL::MG_Config {
|
|||||||
// 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 DisableUboRing = false;
|
||||||
|
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||||
|
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||||
|
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||||
|
// Adreno does not), which means the emulation is dead code on exactly the stack the
|
||||||
|
// headless suite runs on. This forces it live so the scenarios and the CTS can
|
||||||
|
// exercise the path, and gives the device an A/B lever over the same choice.
|
||||||
|
Bool EsprytForceDepthStencilReadbackEmulation = false;
|
||||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||||
// version request.
|
// version request.
|
||||||
Bool RelaxedSemantics = false;
|
Bool RelaxedSemantics = false;
|
||||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
|
||||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
|
||||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
|
||||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
|
||||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||||
@@ -169,6 +203,28 @@ namespace MobileGL::MG_Config {
|
|||||||
// immediately stay serial by their own construction). Off by default; never
|
// immediately stay serial by their own construction). Off by default; never
|
||||||
// advertise it.
|
// advertise it.
|
||||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
|
||||||
|
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
|
||||||
|
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
|
||||||
|
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
|
||||||
|
// ON; ForceOff turns ALL THREE off and makes every translation run from
|
||||||
|
// scratch. The escape hatch exists because a wrong cache hit is a silently
|
||||||
|
// miscompiled shader: if a device ever renders differently with the cache
|
||||||
|
// on, one run with this falsy says so.
|
||||||
|
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||||
|
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||||
|
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||||
|
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||||
|
// it is ON even where the driver advertises GL_OES_viewport_array, because that
|
||||||
|
// extension only ever gave the SHADER a compilable name: MobileGL has never
|
||||||
|
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
|
||||||
|
// and nothing else), so on an extension-capable driver every index rasterized as
|
||||||
|
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
|
||||||
|
// the pre-emulation path, extension passthrough where it exists and
|
||||||
|
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||||
|
// the negative control the emulation is measured against.
|
||||||
|
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -162,11 +162,17 @@ 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.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
features.UseAngle = QueryEnvFlag("MOBILEGL_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.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||||
|
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||||
|
features.FixIterationRPSubgroupScratch =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
|
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
||||||
|
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
||||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
features.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);
|
||||||
@@ -176,8 +182,9 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
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.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||||
|
features.EsprytForceDepthStencilReadbackEmulation =
|
||||||
|
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
|
||||||
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.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
@@ -187,6 +194,9 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
features.AsyncOptimisticShaderStatus =
|
features.AsyncOptimisticShaderStatus =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
|
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||||
|
features.ViewportArrayEmulation =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
+13
-3
@@ -52,11 +52,15 @@
|
|||||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
||||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
||||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
||||||
|
//
|
||||||
|
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
|
||||||
|
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
|
||||||
|
// drops only D. Any edit here must be mirrored in Log.h.
|
||||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -91,6 +95,12 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// =============================== Utils ================================ //
|
// =============================== Utils ================================ //
|
||||||
|
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
|
||||||
|
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
|
||||||
|
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
|
||||||
|
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
|
||||||
|
// 2026-08-13 renumbering unchanged; the contract is and stays
|
||||||
|
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
#define MOBILEGL_ASSERT(condition, ...) \
|
#define MOBILEGL_ASSERT(condition, ...) \
|
||||||
do { \
|
do { \
|
||||||
|
|||||||
@@ -15,8 +15,11 @@
|
|||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -51,6 +54,11 @@ namespace MobileGL {
|
|||||||
// before a re-initialized library could pair them with the wrong
|
// before a re-initialized library could pair them with the wrong
|
||||||
// backend's DeleteSync).
|
// backend's DeleteSync).
|
||||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
|
// Queries die with their contexts for the same reason, and their registry
|
||||||
|
// is the same shape of process-global map: drain it here too, while the
|
||||||
|
// function table can still pair each backend handle with the backend that
|
||||||
|
// minted it.
|
||||||
|
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
@@ -66,6 +74,14 @@ namespace MobileGL {
|
|||||||
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||||
// make the next Initialize() skip a prewarm it genuinely needs.
|
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||||
|
// The two-level translation memo. Nothing in it references a glslang object -
|
||||||
|
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
|
||||||
|
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
|
||||||
|
// fordebug build gets one line per level saying how the run went.
|
||||||
|
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
|
||||||
|
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
|
||||||
|
MG_State::GLState::LogProgramTranslationCacheStats();
|
||||||
|
MG_State::GLState::ClearProgramTranslationCache();
|
||||||
MG_Backend::gBackendFunctionsTable = {};
|
MG_Backend::gBackendFunctionsTable = {};
|
||||||
g_isInitialized = false;
|
g_isInitialized = false;
|
||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace MobileGL {
|
|||||||
namespace MG_State::GLState {
|
namespace MG_State::GLState {
|
||||||
class FramebufferObject;
|
class FramebufferObject;
|
||||||
class ITextureObject;
|
class ITextureObject;
|
||||||
|
class RenderbufferObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class BackendType {
|
enum class BackendType {
|
||||||
@@ -24,6 +25,19 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
|
|
||||||
namespace MG_Backend {
|
namespace MG_Backend {
|
||||||
|
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
|
||||||
|
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
|
||||||
|
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
|
||||||
|
// one of the two pointers is set; neither is set when the name named nothing, which is
|
||||||
|
// the INVALID_VALUE the frontend validator reports.
|
||||||
|
struct CopyImageEndpoint {
|
||||||
|
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||||
|
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||||
|
|
||||||
|
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
|
||||||
|
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
|
||||||
|
};
|
||||||
|
|
||||||
enum class FormatCapability : Uint64 {
|
enum class FormatCapability : Uint64 {
|
||||||
Creatable = 1ull << 0,
|
Creatable = 1ull << 0,
|
||||||
|
|
||||||
@@ -160,9 +174,9 @@ namespace MobileGL {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
GLsizei width, GLsizei height);
|
GLsizei width, GLsizei height);
|
||||||
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
const CopyImageEndpoint& dst,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void (*GenerateMipmap)(GLenum target);
|
void (*GenerateMipmap)(GLenum target);
|
||||||
@@ -236,6 +250,14 @@ namespace MobileGL {
|
|||||||
// (optional; null = frontend falls back to CPU accounting).
|
// (optional; null = frontend falls back to CPU accounting).
|
||||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||||
|
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
|
||||||
|
// frontend's own accounting wherever that accounting is exact - a capture with no
|
||||||
|
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
|
||||||
|
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
|
||||||
|
// the written count for a vertex-only capture that follows a large render pass,
|
||||||
|
// where the desktop-exact answer is the one the frontend already computed. Defaults
|
||||||
|
// to false, so a backend that never sets it keeps using its GPU result.
|
||||||
|
Bool PrefersCpuXfbPrimitiveAccounting = false;
|
||||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||||
@@ -318,6 +340,22 @@ namespace MobileGL {
|
|||||||
Int MaxVertexAttribs = 16;
|
Int MaxVertexAttribs = 16;
|
||||||
Int MaxComputeShaderStorageBlocks = 8;
|
Int MaxComputeShaderStorageBlocks = 8;
|
||||||
Int MaxCombinedShaderStorageBlocks = 32;
|
Int MaxCombinedShaderStorageBlocks = 32;
|
||||||
|
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
|
||||||
|
// non-compute, non-fragment stages and these defaults are the spec minimums, not
|
||||||
|
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
|
||||||
|
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
|
||||||
|
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
|
||||||
|
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
|
||||||
|
// backend that cannot honour a graphics-stage storage block MUST report 0 here
|
||||||
|
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
|
||||||
|
// does not make the block work; it only moves the failure from an honest
|
||||||
|
// "unsupported" at query time to a backend link error the frontend never surfaces,
|
||||||
|
// after which every draw with that program silently renders nothing.
|
||||||
|
Int MaxVertexShaderStorageBlocks = 0;
|
||||||
|
Int MaxTessControlShaderStorageBlocks = 0;
|
||||||
|
Int MaxTessEvaluationShaderStorageBlocks = 0;
|
||||||
|
Int MaxGeometryShaderStorageBlocks = 0;
|
||||||
|
Int MaxFragmentShaderStorageBlocks = 8;
|
||||||
Int MaxComputeUniformBlocks = 12;
|
Int MaxComputeUniformBlocks = 12;
|
||||||
Int MaxComputeWorkGroupInvocations = 128;
|
Int MaxComputeWorkGroupInvocations = 128;
|
||||||
Int MaxShaderStorageBufferBindings = 8;
|
Int MaxShaderStorageBufferBindings = 8;
|
||||||
@@ -334,8 +372,32 @@ namespace MobileGL {
|
|||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
|
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
||||||
|
// backend that cannot host a clip distance MUST report it: advertising eight the
|
||||||
|
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
||||||
|
// from an honest "unsupported" at query time to a backend shader-compile error the
|
||||||
|
// frontend never surfaces, after which every draw with that program silently renders
|
||||||
|
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
||||||
|
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
||||||
|
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
||||||
|
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||||
|
// hand glslang a workable gl_MaxClipDistances.
|
||||||
Int MaxClipDistances = 8;
|
Int MaxClipDistances = 8;
|
||||||
Int MaxViewports = 16;
|
Int MaxViewports = 16;
|
||||||
|
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||||
|
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||||
|
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
|
||||||
|
// a convention is a statement about behaviour, so a backend that does not pin one
|
||||||
|
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
|
||||||
|
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
|
||||||
|
// capability is absent: without the viewport array extension only viewport 0 is ever
|
||||||
|
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
|
||||||
|
// both - which vertex provokes is decided per pipeline by
|
||||||
|
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
||||||
|
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
||||||
|
// of the backend.
|
||||||
|
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
Int MaxViewportWidth = 16384;
|
Int MaxViewportWidth = 16384;
|
||||||
Int MaxViewportHeight = 16384;
|
Int MaxViewportHeight = 16384;
|
||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
@@ -383,12 +445,36 @@ namespace MobileGL {
|
|||||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||||
}
|
}
|
||||||
|
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
|
||||||
|
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
|
||||||
|
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
|
||||||
|
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
|
||||||
|
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
|
||||||
|
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
|
||||||
|
// both report VK_FALSE, so no real mobile device does.
|
||||||
|
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
|
||||||
|
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
|
||||||
|
// profile") and the demotion there is mathematically mandatory, always.
|
||||||
|
// Defaults to false so a backend that never sets it - and the no-backend case, which
|
||||||
|
// is what standalone shader compiles and the unit tests run under - keeps the
|
||||||
|
// demotion, which is the behaviour that works everywhere.
|
||||||
|
Bool SupportsShaderFloat64 = false;
|
||||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||||
|
//
|
||||||
|
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
|
||||||
|
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
|
||||||
|
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
|
||||||
|
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
|
||||||
|
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
|
||||||
|
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
|
||||||
|
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
|
||||||
|
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||||
|
// still see one consistent world.
|
||||||
Bool SupportsFloat64VertexAttributes = false;
|
Bool SupportsFloat64VertexAttributes = false;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "BackendObject_DirectGLES.h"
|
#include "BackendObject_DirectGLES.h"
|
||||||
#include "MG_Backend/BackendObject.h"
|
#include "MG_Backend/BackendObject.h"
|
||||||
|
#include "MG_Backend/BackendObjects.h"
|
||||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
#include <MG_Backend/DirectGLES/Managers.h>
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
#include <MG_Backend/DirectGLES/Utils.h>
|
#include <MG_Backend/DirectGLES/Utils.h>
|
||||||
@@ -212,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
}
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
// A format is either 8- or 16-bit signed normalized, so at most one of the two ever
|
||||||
|
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
|
||||||
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
reasons.push_back("EXT_render_snorm not supported");
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,9 +410,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return complete;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `samples` only reaches the multisample targets; every other target ignores it. The
|
||||||
|
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
|
||||||
|
// repeating the gen/bind/completeness/delete dance.
|
||||||
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
|
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
|
||||||
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
|
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
|
||||||
Bool* outRenderable) {
|
Bool* outRenderable, Int samples = 1) {
|
||||||
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
|
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -428,10 +435,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
|
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
|
||||||
if (isMultisample) {
|
if (isMultisample) {
|
||||||
|
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
|
||||||
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
|
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
|
||||||
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE);
|
gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
|
||||||
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
|
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
|
||||||
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
|
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
|
||||||
} else {
|
} else {
|
||||||
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
|
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
|
||||||
gl.glDeleteTextures(1, &texture);
|
gl.glDeleteTextures(1, &texture);
|
||||||
@@ -527,6 +535,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return sampleCounts;
|
return sampleCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
|
||||||
|
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
|
||||||
|
// for every format on the multisample targets even where glTexImage2DMultisample happily
|
||||||
|
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
|
||||||
|
// two answers cannot both be right. Completeness is required at every count, exactly as
|
||||||
|
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
|
||||||
|
// probe has already succeeded, so 1 terminates the list without being re-probed.
|
||||||
|
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
||||||
|
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
|
||||||
|
TextureInternalFormat logicalFormat, Int maxSamples) {
|
||||||
|
Vector<Int> sampleCounts;
|
||||||
|
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
|
||||||
|
Bool renderable = false;
|
||||||
|
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
|
||||||
|
&renderable, samples);
|
||||||
|
if (created && renderable) {
|
||||||
|
sampleCounts.push_back(samples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sampleCounts.push_back(1);
|
||||||
|
return sampleCounts;
|
||||||
|
}
|
||||||
|
|
||||||
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
|
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
|
||||||
const MG_External::GLESCapabilities& capabilities,
|
const MG_External::GLESCapabilities& capabilities,
|
||||||
FormatCapabilityCache& cache) {
|
FormatCapabilityCache& cache) {
|
||||||
@@ -627,7 +658,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||||
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
|
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
if (IsGLESProbeMultisampleTarget(target)) {
|
||||||
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
const Int maxSamples =
|
||||||
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
||||||
|
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
||||||
|
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||||
|
nativeInfo.ImageType, logicalFormat, maxSamples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shouldProbeFallback = !nativeCreated || !nativeRenderable;
|
shouldProbeFallback = !nativeCreated || !nativeRenderable;
|
||||||
@@ -645,7 +680,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
||||||
}
|
}
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
if (IsGLESProbeMultisampleTarget(target)) {
|
||||||
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
const Int maxSamples =
|
||||||
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
||||||
|
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
||||||
|
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||||
|
fallbackInfo.ImageType, logicalFormat, maxSamples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,9 +751,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
{
|
{
|
||||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||||
},
|
},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||||
@@ -734,9 +773,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// thread can only observe the extension string after the
|
// thread can only observe the extension string after the
|
||||||
// advertisement for its context has settled; rebuilding the whole
|
// advertisement for its context has settled; rebuilding the whole
|
||||||
// list keeps the re-run after a context recreation idempotent.
|
// list keeps the re-run after a context recreation idempotent.
|
||||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||||
|
capabilities.SupportsDrawIndirect,
|
||||||
|
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -745,6 +786,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
||||||
|
Int samples) {
|
||||||
|
if (samples <= 1) {
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
Int maxSamples = 0;
|
||||||
|
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
|
||||||
|
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
|
||||||
|
formatIndex < kFormatCapabilityFormatCount) {
|
||||||
|
// Descending, so the head is the largest count this device actually allocated.
|
||||||
|
const Vector<Int>& probedCounts =
|
||||||
|
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||||
|
if (!probedCounts.empty()) {
|
||||||
|
maxSamples = probedCounts.front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxSamples <= 0) {
|
||||||
|
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
|
||||||
|
}
|
||||||
|
return std::min(samples, std::max(maxSamples, 1));
|
||||||
|
}
|
||||||
|
|
||||||
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
||||||
DestroyEGLContext();
|
DestroyEGLContext();
|
||||||
}
|
}
|
||||||
@@ -779,11 +843,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
// reconcile every runtime-gated advertisement (see the comment on
|
||||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||||
// list is first built).
|
// built).
|
||||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -924,11 +988,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return MutableRendererInfo();
|
return MutableRendererInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||||
|
Bool drawIndirectSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||||
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
||||||
@@ -940,10 +1006,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// picks a whole different shader for draw_buffers without
|
// picks a whole different shader for draw_buffers without
|
||||||
// explicit_attrib_location. DirectVulkan advertises both.
|
// explicit_attrib_location. DirectVulkan advertises both.
|
||||||
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
|
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
|
||||||
|
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||||
|
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||||
|
// version: a caller that finds the extension missing never resolves
|
||||||
|
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||||
|
// blocks anyway calls through a null pointer.
|
||||||
|
E_GL_ARB_uniform_buffer_object,
|
||||||
|
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||||
|
// so on a 4.0 context the string is the only way to reach it. The host ES driver
|
||||||
|
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||||
|
// runs on provides.
|
||||||
|
E_GL_ARB_stencil_texturing,
|
||||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
E_GL_ARB_get_program_binary};
|
E_GL_ARB_get_program_binary};
|
||||||
|
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||||
|
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||||
|
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||||
|
if (drawIndirectSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||||
|
}
|
||||||
|
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||||
|
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||||
|
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||||
|
// that incomplete case.
|
||||||
|
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_base_instance);
|
||||||
|
}
|
||||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||||
@@ -1079,6 +1169,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// geometry shader's amplification.
|
// geometry shader's amplification.
|
||||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||||
|
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
|
||||||
|
// own count is the desktop-exact one and the ES driver's is only as good as the
|
||||||
|
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
|
||||||
|
// follows a large render pass). The query above stays installed: it is still what
|
||||||
|
// answers an amplifying span, and PRIMITIVES_GENERATED always.
|
||||||
|
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
||||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
@@ -1158,9 +1254,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
|
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
|
||||||
|
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
|
||||||
|
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
|
||||||
|
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
|
||||||
|
// graphics stage except fragment) and is the only answer that lets an application take
|
||||||
|
// its own fallback instead of building a program the driver will refuse to link. The
|
||||||
|
// stage limit cannot exceed the combined limit or the number of binding points there
|
||||||
|
// are to bind buffers to, so clamp to both.
|
||||||
|
const auto clampStageStorageBlocks = [this](Int stageLimit) {
|
||||||
|
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
||||||
|
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
|
||||||
|
};
|
||||||
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||||
|
m_dynamicParameters.MaxVertexShaderStorageBlocks =
|
||||||
|
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
|
||||||
|
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
|
||||||
|
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
|
||||||
|
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
|
||||||
|
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
|
||||||
|
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
||||||
|
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
|
||||||
|
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
|
||||||
|
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
|
||||||
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
||||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||||
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
|
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
|
||||||
@@ -1213,14 +1331,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
// Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
|
||||||
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
|
||||||
// land on this backend regardless of what the driver underneath happens to support.
|
// module that still declared Float64 would never reach the driver at all. The demotion is
|
||||||
|
// mathematically mandatory here, on every device, forever - which is why this stays false
|
||||||
|
// regardless of what the driver underneath happens to support.
|
||||||
|
m_dynamicParameters.SupportsShaderFloat64 = false;
|
||||||
|
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||||
|
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||||
|
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
||||||
|
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
||||||
|
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
|
||||||
|
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
|
||||||
|
// describe behaviour this backend does not implement.
|
||||||
|
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
|
||||||
|
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
|
||||||
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const MG_External::GLESCapabilities& capabilities,
|
const MG_External::GLESCapabilities& capabilities,
|
||||||
FormatCapabilityCache& cache);
|
FormatCapabilityCache& cache);
|
||||||
|
|
||||||
|
// Clamps a requested sample count down to what the ES driver can really deliver for this
|
||||||
|
// format on this format-capability target: the probed per-format list when there is one, the
|
||||||
|
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
|
||||||
|
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
|
||||||
|
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
|
||||||
|
// allocation call has to come through here. The shadow state keeps the requested count, so
|
||||||
|
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
|
||||||
|
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
||||||
|
Int samples);
|
||||||
|
|
||||||
class BackendObject_DirectGLES : public BackendObject {
|
class BackendObject_DirectGLES : public BackendObject {
|
||||||
public:
|
public:
|
||||||
~BackendObject_DirectGLES() override;
|
~BackendObject_DirectGLES() override;
|
||||||
@@ -67,9 +77,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const RendererInfo& GetRendererIdentity();
|
const RendererInfo& GetRendererIdentity();
|
||||||
|
|
||||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||||
|
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||||
|
Bool drawIndirectSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported);
|
||||||
|
|
||||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height);
|
GLsizei height);
|
||||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
const CopyImageEndpoint& dst,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
||||||
|
|
||||||
|
// The ESSL half of the gl_ViewportIndex routing emulation, in the order a program's stages
|
||||||
|
// meet it. Both are pure String -> String rewrites over what SPIRV-Cross emitted once
|
||||||
|
// LowerViewportIndexPass has demoted the builtin to the plain global `mg_ViewportIndex`.
|
||||||
|
//
|
||||||
|
// The producing stage's global becomes an ordinary flat varying; true when there was one to
|
||||||
|
// promote, which is also the answer to "does this program route viewports at all".
|
||||||
|
Bool PromoteViewportIndexGlobalToVarying(String& source);
|
||||||
|
// The fragment stage grows a matching flat input, the mg_ViewportPassMask uniform the draw
|
||||||
|
// path writes, and a wrapper entry point that discards every fragment whose primitive routed
|
||||||
|
// to an index the current replay pass is not drawing. False when the stage has no entry point
|
||||||
|
// to wrap, which leaves the program renderable but unrouted.
|
||||||
|
Bool InjectViewportIndexPassGate(String& source);
|
||||||
|
|
||||||
|
// Whether a vertex shader may declare a storage block at all, given what the host driver
|
||||||
|
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
|
||||||
|
// global purely so the decision can be tested without one.
|
||||||
|
//
|
||||||
|
// The indirect half of the gl_BaseInstance lowering in PromoteDrawParameterGlobalsToUniforms
|
||||||
|
// is the only thing that needs this, and it needs exactly one block. A driver reporting 0 is
|
||||||
|
// conformant - the minimum is 0 in GL 4.6 table 23.64 and ES 3.2 table 21.44 - and ARM's
|
||||||
|
// GLES driver does report 0, so this is a live path, not a defensive one.
|
||||||
|
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks);
|
||||||
|
|
||||||
// True once the process has entered exit(): past that point the EGL library and
|
// True once the process has entered exit(): past that point the EGL library and
|
||||||
// the driver may already be unloaded, so a backend twin's destructor must not
|
// the driver may already be unloaded, so a backend twin's destructor must not
|
||||||
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
||||||
@@ -103,6 +126,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// link.
|
// link.
|
||||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||||
|
|
||||||
|
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
||||||
|
//
|
||||||
|
// GLES has ONE viewport, ONE scissor rectangle and ONE depth range; GL 4.1 has sixteen of
|
||||||
|
// each, selected per primitive by gl_ViewportIndex. There is no ES entry point to program the
|
||||||
|
// other fifteen with (GL_OES_viewport_array exists but Adreno 830 does not have it, verified
|
||||||
|
// three ways), so the only way to rasterize a primitive against index i's rectangle is to
|
||||||
|
// make index i's rectangle THE viewport for the duration of a draw - which means issuing the
|
||||||
|
// draw once per distinct viewport state and letting the fragment stage throw away the
|
||||||
|
// primitives that belong to the other indices (the gate Managers.cpp injects).
|
||||||
|
//
|
||||||
|
// Indices whose whole state tuple (viewport rectangle, scissor rectangle, scissor-test enable,
|
||||||
|
// depth range) is identical share ONE pass, so the overwhelmingly common case - every index
|
||||||
|
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
||||||
|
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
||||||
|
//
|
||||||
|
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||||
|
// restores the pre-emulation path as a negative control.
|
||||||
|
Bool ViewportArrayEmulationEnabled();
|
||||||
|
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||||
|
// true; it exists so that BeginViewportRoutingPasses - which runs on every draw of every
|
||||||
|
// workload - can answer with one static load in the case that matters, which is every
|
||||||
|
// application that has never heard of gl_ViewportIndex.
|
||||||
|
extern Bool g_anyProgramRoutesViewportIndex;
|
||||||
|
// Number of times the current draw has to be issued. Always >= 1, and exactly 1 - with no
|
||||||
|
// state touched - whenever the current program does not route viewports, whenever every
|
||||||
|
// configured index shares one state, and whenever replaying would multiply a side effect the
|
||||||
|
// fragment gate cannot undo (transform feedback, rasterizer discard). Also seeds the pass
|
||||||
|
// mask uniform for that single-pass case, so a gated fragment shader never runs against the
|
||||||
|
// zero every GLSL uniform starts at - which would discard the whole draw.
|
||||||
|
Uint BeginViewportRoutingPasses();
|
||||||
|
// Push pass `pass`'s viewport / scissor / scissor-test / depth range onto the ES context and
|
||||||
|
// set the gate mask to the indices it serves. Only called when the count above exceeds 1.
|
||||||
|
void ApplyViewportRoutingPass(Uint pass);
|
||||||
|
// Restore the gate mask and mark the render-state shadow dirty, so the next ordinary draw
|
||||||
|
// re-pushes index 0's state. Takes the count so it can do nothing at all in the common case.
|
||||||
|
void EndViewportRoutingPasses(Uint passCount);
|
||||||
|
|
||||||
|
// Issue one draw, replayed once per viewport-routing pass. Every application-visible draw
|
||||||
|
// entry point wraps its native glDraw* call in this; the internal blit and clear helpers
|
||||||
|
// deliberately do not, because they bind their own programs, which never route.
|
||||||
|
template <typename IssueDraw>
|
||||||
|
inline void ForEachViewportRoutingPass(IssueDraw&& issue) {
|
||||||
|
const Uint passCount = BeginViewportRoutingPasses();
|
||||||
|
for (Uint pass = 0; pass < passCount; ++pass) {
|
||||||
|
if (passCount > 1) {
|
||||||
|
ApplyViewportRoutingPass(pass);
|
||||||
|
}
|
||||||
|
issue();
|
||||||
|
}
|
||||||
|
EndViewportRoutingPasses(passCount);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename StateObject, typename BackendObject>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
class StateBackendObjectRegistry {
|
||||||
public:
|
public:
|
||||||
@@ -129,7 +204,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Twin creation is the moment a driver-owned id starts needing a guarded
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||||
// destructor; cold path, so the once-guard costs nothing per draw.
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
||||||
EnsureProcessTeardownSentinel();
|
EnsureProcessTeardownSentinel();
|
||||||
|
// Sweep BEFORE the entry reference below exists: the map is open-addressed and an
|
||||||
|
// erase relocates the rest of the probe cluster, so collecting once that reference
|
||||||
|
// is taken would invalidate it. The sweep is therefore owed from an earlier call
|
||||||
|
// rather than triggered by this one.
|
||||||
|
if (m_creationTick >= kCreationGCInterval) {
|
||||||
|
m_creationTick = 0;
|
||||||
|
CollectGarbage();
|
||||||
|
}
|
||||||
|
const SizeT entryCountBeforeInsert = m_entries.size();
|
||||||
auto& entry = m_entries[stateObj.get()];
|
auto& entry = m_entries[stateObj.get()];
|
||||||
|
if (m_entries.size() != entryCountBeforeInsert) {
|
||||||
|
// A key the registry has never held. Nothing tells the backend that a texture or
|
||||||
|
// renderbuffer was DELETED - the twin, and the driver storage it owns, lives
|
||||||
|
// until a collection - and CollectGarbageIfNeeded is ticked only from the
|
||||||
|
// per-draw sync paths, which a CTS-shaped workload runs about ten times per
|
||||||
|
// case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead
|
||||||
|
// (and, for this suite, gigabyte-sized) objects stay allocated at once. Object
|
||||||
|
// CHURN rather than draw count is what makes the sweep urgent, so a twin the
|
||||||
|
// registry has never seen ticks it too - and it does so on the path that is
|
||||||
|
// about to allocate, which is exactly when the memory is needed.
|
||||||
|
++m_creationTick;
|
||||||
|
}
|
||||||
if (entry.stateRef.expired()) {
|
if (entry.stateRef.expired()) {
|
||||||
// The previous owner of this address is gone and the allocator handed it
|
// The previous owner of this address is gone and the allocator handed it
|
||||||
// to a new object: its twin describes ids the new state object never made.
|
// to a new object: its twin describes ids the new state object never made.
|
||||||
@@ -203,8 +299,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr Uint32 kGCInterval = 1024;
|
static constexpr Uint32 kGCInterval = 1024;
|
||||||
|
// Creations are far rarer than draws, so this counts in a much smaller unit than
|
||||||
|
// kGCInterval does.
|
||||||
|
static constexpr Uint32 kCreationGCInterval = 64;
|
||||||
BackendMap m_entries;
|
BackendMap m_entries;
|
||||||
Uint32 m_gcTick = 0;
|
Uint32 m_gcTick = 0;
|
||||||
|
Uint32 m_creationTick = 0;
|
||||||
Bool m_isCollecting = false;
|
Bool m_isCollecting = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +386,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// context loss.
|
// context loss.
|
||||||
Bool persistentMapped = false;
|
Bool persistentMapped = false;
|
||||||
void* persistentPtr = nullptr;
|
void* persistentPtr = nullptr;
|
||||||
|
// The GL store behind `id` was created with glBufferStorageEXT and is
|
||||||
|
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
|
||||||
|
// recycled through the size-keyed buffer pool. Tracked separately from
|
||||||
|
// persistentMapped because the two come apart: a glMapBufferRange that fails
|
||||||
|
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
|
||||||
|
// no map, and a respecification then has to retire the id rather than hand it
|
||||||
|
// to glBufferData, which the driver would silently refuse.
|
||||||
|
Bool immutableStorage = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||||
@@ -338,6 +446,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||||
// could false-skip when the name is recycled.
|
// could false-skip when the name is recycled.
|
||||||
void NoteBufferIdDeleted(Uint id);
|
void NoteBufferIdDeleted(Uint id);
|
||||||
|
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
|
||||||
|
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
|
||||||
|
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
|
||||||
|
// key on FRONTEND versions, which a backend-side re-mint does not move - without
|
||||||
|
// this generation the driver VAO would keep fetching through the deleted id (or
|
||||||
|
// its retained store) forever. Compared and stamped by
|
||||||
|
// BackendVertexArrayObject::SyncToBackend.
|
||||||
|
extern Uint64 g_bufferBackendIdGeneration;
|
||||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||||
@@ -345,6 +461,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||||
void InvalidateIndexedBufferBindingCache();
|
void InvalidateIndexedBufferBindingCache();
|
||||||
|
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
||||||
|
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
||||||
|
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
||||||
|
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
|
||||||
|
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
|
||||||
|
// application bound never reaches the driver.
|
||||||
|
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
|
||||||
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
||||||
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
||||||
// without glDeleteBuffers (called when the ES context is going away).
|
// without glDeleteBuffers (called when the ES context is going away).
|
||||||
@@ -451,12 +574,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Narrows one enabled GL_DOUBLE array into a tightly packed float32 stream held in
|
||||||
|
// this VAO's own scratch buffer and declares the attribute against it. ES has no
|
||||||
|
// 64-bit vertex format, but the source bytes are ordinary IEEE-754 doubles and every
|
||||||
|
// fp64 value in every shader is already narrowed to 32 bits (DemoteFloat64Pass), so
|
||||||
|
// narrowing the ARRAY is the coherent completion of that decision rather than
|
||||||
|
// dropping it. Returns false when the stream cannot be built, in which case the
|
||||||
|
// caller must DISABLE the array - leaving a 64-bit array enabled with no pointer is
|
||||||
|
// what the Adreno driver turns into a SIGSEGV at the next draw.
|
||||||
|
Bool SyncFloat64AttributeAsFloat32(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib,
|
||||||
|
Uint32 fetchBaseInstance);
|
||||||
|
|
||||||
|
// What the converted float32 stream in m_convertedAttributeBufferIds[i] was built
|
||||||
|
// from. A hit skips the CPU conversion and the re-upload; the buffer's change serial
|
||||||
|
// is part of the key, so a glBufferSubData into the source invalidates it.
|
||||||
|
struct ConvertedFloat64Stream {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint64 sourceLifetimeId = 0;
|
||||||
|
Uint64 sourceChangeSerial = 0;
|
||||||
|
SizeT sourceOffset = 0;
|
||||||
|
SizeT sourceStride = 0;
|
||||||
|
SizeT componentCount = 0;
|
||||||
|
SizeT elementCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
PendingAttribValueMask m_pendingAttribValueMask;
|
PendingAttribValueMask m_pendingAttribValueMask;
|
||||||
Uint m_backendVAOId = 0;
|
Uint m_backendVAOId = 0;
|
||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
|
// Scratch stores for the buffer-backed GL_DOUBLE narrowing. Deliberately separate
|
||||||
|
// from m_clientAttributeBufferIds: that one holds the per-draw upload of a
|
||||||
|
// CLIENT-MEMORY array, and an attribute index can carry both shapes over its life.
|
||||||
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_convertedAttributeBufferIds;
|
||||||
|
Array<ConvertedFloat64Stream, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||||
|
m_convertedAttributeStreams;
|
||||||
|
// True while at least one attribute of this VAO is fed by a converted stream. Such a
|
||||||
|
// stream is derived from buffer CONTENT, which no VAO version covers, so the config
|
||||||
|
// version early-out in SyncToBackend must not be trusted while it is set.
|
||||||
|
Bool m_hasConvertedFloat64Attribute = false;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
|
// Identity of the buffer the version above was stamped against. Raw and never
|
||||||
|
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
|
||||||
|
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
|
||||||
|
// alone would read a wrapped-back count with a different buffer bound as clean.
|
||||||
|
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
|
||||||
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
||||||
// version on every per-attribute version bump (the three Bump*Version functions are
|
// version on every per-attribute version bump (the three Bump*Version functions are
|
||||||
// its only writers), so an unchanged config version proves every per-attribute
|
// its only writers), so an unchanged config version proves every per-attribute
|
||||||
@@ -472,6 +634,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Kept here because it describes what was last EMITTED, which is what the next sync
|
// Kept here because it describes what was last EMITTED, which is what the next sync
|
||||||
// has to correct.
|
// has to correct.
|
||||||
Uint32 m_syncedFetchBaseInstance = 0;
|
Uint32 m_syncedFetchBaseInstance = 0;
|
||||||
|
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
|
||||||
|
// mismatch means some live buffer's driver id was re-minted since; the ids
|
||||||
|
// baked into the driver VAO's attribute/element bindings may be dead even
|
||||||
|
// though every frontend version matches, so the next sync re-emits them all.
|
||||||
|
Uint64 m_syncedBufferIdGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||||
@@ -578,9 +745,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
||||||
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
||||||
// scratch buffer and has to outlive the returned pointer.
|
// scratch buffer and has to outlive the returned pointer.
|
||||||
|
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
|
||||||
|
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
|
||||||
|
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
|
||||||
|
// transfer type's own "ones" is.
|
||||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||||
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||||
Bool integerData = false);
|
Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
|
||||||
|
|
||||||
|
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
|
||||||
|
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
|
||||||
|
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
|
||||||
|
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
|
||||||
|
// returned pointer.
|
||||||
|
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
|
||||||
|
Vector<Uint8>& widenedData);
|
||||||
|
|
||||||
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
@@ -615,10 +794,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void RequireImageBindableStorage();
|
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||||
|
// non-core image format means re-minting it in the widening's carrier. Takes the state
|
||||||
|
// object because the levels already uploaded have to be marked dirty again: the
|
||||||
|
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
|
||||||
|
void RequireImageBindableStorage(
|
||||||
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
|
// Whether this texture's ES storage was minted in an image carrier rather than in the
|
||||||
|
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
|
||||||
|
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
|
||||||
|
// the application floats.
|
||||||
|
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
|
||||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||||
Uint GetBackendTextureId() const;
|
Uint GetBackendTextureId() const;
|
||||||
|
|
||||||
|
// The id to hand glBindImageTexture for a SPLIT buffer image, or 0 when this texture
|
||||||
|
// takes no split. See m_bufferImageSplitViewId.
|
||||||
|
Uint GetBufferImageSplitViewId() const { return m_bufferImageSplitViewId; }
|
||||||
|
|
||||||
// Aggregate first-level clean gate for the per-draw trio
|
// Aggregate first-level clean gate for the per-draw trio
|
||||||
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
||||||
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
||||||
@@ -655,6 +848,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void RecreateBackendTexture();
|
void RecreateBackendTexture();
|
||||||
|
|
||||||
Uint m_backendTextureId = 0;
|
Uint m_backendTextureId = 0;
|
||||||
|
// A SECOND buffer-texture name over the SAME buffer object, viewed in the split's
|
||||||
|
// single-channel base format, used only as the glBindImageTexture target.
|
||||||
|
//
|
||||||
|
// The split needs the view to say r32f where the application said rg32f, but a buffer
|
||||||
|
// texture that is image-bound may ALSO be read through a samplerBuffer - and the
|
||||||
|
// sampler side is not subscript-rewritten, so re-describing the application's own
|
||||||
|
// texture broke it: texelFetch(s, i) returned component 2i of the base view instead of
|
||||||
|
// texel i's pair. That is exactly and only
|
||||||
|
// KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess, which image-stores
|
||||||
|
// into a GL_RG32F buffer texture and then reads the same texture through both an
|
||||||
|
// imageBuffer and a samplerBuffer in one shader, comparing the two.
|
||||||
|
//
|
||||||
|
// Two names over one buffer cost nothing and alias exactly: a buffer texture owns no
|
||||||
|
// storage, so both views are the application's bytes, and the split's whole premise is
|
||||||
|
// that the two describe the same memory. The application's own name therefore keeps
|
||||||
|
// the format it asked for - rg32f IS a legal SAMPLED buffer-texture format in ES 3.2,
|
||||||
|
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
||||||
|
// the split the shader was rewritten against. 0 when this texture takes no split.
|
||||||
|
Uint m_bufferImageSplitViewId = 0;
|
||||||
// ES context generation the id was created under; a dtor running after
|
// ES context generation the id was created under; a dtor running after
|
||||||
// that context died must not delete a foreign (recycled) name.
|
// that context died must not delete a foreign (recycled) name.
|
||||||
Uint m_contextGeneration = 0;
|
Uint m_contextGeneration = 0;
|
||||||
@@ -691,8 +903,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||||
|
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||||
|
// texture that never asks for the stencil aspect never emits the call. The
|
||||||
|
// depth/stencil readback and replicate-blit emulations also write this parameter
|
||||||
|
// raw, but only ever on their own scratch textures (never on an application
|
||||||
|
// texture), so they cannot desynchronise this cache.
|
||||||
|
GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||||
Uint16 m_syncedSamplerVersion = 0;
|
Uint16 m_syncedSamplerVersion = 0;
|
||||||
Uint16 m_syncedTextureParamsVersion = 0;
|
Uint16 m_syncedTextureParamsVersion = 0;
|
||||||
|
// Set when the driver texture underneath was regenerated and has therefore lost every
|
||||||
|
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||||
|
// once, or an unchanged version would skip the re-push forever.
|
||||||
|
Bool m_forceTextureParamsResync = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ActivateTextureUnit(Uint unit);
|
void ActivateTextureUnit(Uint unit);
|
||||||
@@ -781,6 +1003,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||||
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
||||||
|
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
|
||||||
|
// mismatch means some backend texture id was re-minted since, and any of this
|
||||||
|
// twin's attachment points may still hold the dead id even though the frontend
|
||||||
|
// attachment versions match - so the walk re-attaches everything first.
|
||||||
|
Uint64 m_syncedBackendIdGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||||
@@ -870,6 +1097,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||||
g_fboSyncedObjects;
|
g_fboSyncedObjects;
|
||||||
|
|
||||||
|
// Bumped whenever a live backend texture's driver id is re-minted while its
|
||||||
|
// frontend texture may still be attached to application FBOs
|
||||||
|
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
|
||||||
|
// whose backend storage went immutable). The FBO twins' attachment memos key on
|
||||||
|
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
|
||||||
|
// the driver FBO would keep the deleted texture name attached forever. The
|
||||||
|
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
||||||
|
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
||||||
|
extern Uint64 g_attachmentBackendIdGeneration;
|
||||||
|
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
||||||
|
// target; part of the synced tuple above.
|
||||||
|
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
|
||||||
|
|
||||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||||
@@ -973,23 +1213,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||||
// assigned through glUniform1i.
|
// assigned through glUniform1i.
|
||||||
|
//
|
||||||
|
// ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated
|
||||||
|
// (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list
|
||||||
|
// used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable
|
||||||
|
// shortcut and was two bugs: an image uniform this says "no" to is one
|
||||||
|
// CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor
|
||||||
|
// widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not
|
||||||
|
// supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and
|
||||||
|
// assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY
|
||||||
|
// image - which ES 3.2 has in core, so it is not even an emulated target - hit both.
|
||||||
inline Bool IsImageUniformType(GLenum type) {
|
inline Bool IsImageUniformType(GLenum type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 0x904C: /*GL_IMAGE_1D*/
|
||||||
case 0x904D: /*GL_IMAGE_2D*/
|
case 0x904D: /*GL_IMAGE_2D*/
|
||||||
case 0x904E: /*GL_IMAGE_3D*/
|
case 0x904E: /*GL_IMAGE_3D*/
|
||||||
|
case 0x904F: /*GL_IMAGE_2D_RECT*/
|
||||||
case 0x9050: /*GL_IMAGE_CUBE*/
|
case 0x9050: /*GL_IMAGE_CUBE*/
|
||||||
case 0x9051: /*GL_IMAGE_BUFFER*/
|
case 0x9051: /*GL_IMAGE_BUFFER*/
|
||||||
|
case 0x9052: /*GL_IMAGE_1D_ARRAY*/
|
||||||
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
||||||
|
case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/
|
||||||
|
case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/
|
||||||
|
case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||||
|
case 0x9057: /*GL_INT_IMAGE_1D*/
|
||||||
case 0x9058: /*GL_INT_IMAGE_2D*/
|
case 0x9058: /*GL_INT_IMAGE_2D*/
|
||||||
case 0x9059: /*GL_INT_IMAGE_3D*/
|
case 0x9059: /*GL_INT_IMAGE_3D*/
|
||||||
|
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
|
||||||
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
||||||
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
||||||
|
case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/
|
||||||
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
||||||
|
case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/
|
||||||
|
case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/
|
||||||
|
case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||||
|
case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/
|
||||||
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
||||||
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
||||||
|
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
|
||||||
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
||||||
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
||||||
|
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/
|
||||||
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
||||||
|
case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/
|
||||||
|
case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/
|
||||||
|
case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -997,6 +1265,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
|
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
|
||||||
|
struct ImageFormatBakeInputs;
|
||||||
|
|
||||||
class BackendProgramObjectImpl {
|
class BackendProgramObjectImpl {
|
||||||
public:
|
public:
|
||||||
// Per-link cache of a sampler-style uniform's backend location: built once in
|
// Per-link cache of a sampler-style uniform's backend location: built once in
|
||||||
@@ -1056,7 +1327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BackendProgramObjectImpl();
|
BackendProgramObjectImpl();
|
||||||
~BackendProgramObjectImpl();
|
~BackendProgramObjectImpl();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
void Use() const;
|
void Use();
|
||||||
void SetBaseInstance(Uint32 baseInstance) const;
|
void SetBaseInstance(Uint32 baseInstance) const;
|
||||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||||
void SetDrawID(Uint32 drawId) const;
|
void SetDrawID(Uint32 drawId) const;
|
||||||
@@ -1067,6 +1338,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||||
// uniform write, and only such a program needs the reset after one.
|
// uniform write, and only such a program needs the reset after one.
|
||||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||||
|
// Which viewport indices the next draw's fragments may keep, one bit each. Written
|
||||||
|
// once per replay pass; see ForEachViewportRoutingPass.
|
||||||
|
void SetViewportPassMask(Uint32 indexMask) const;
|
||||||
|
// True when this build injected the fragment-stage viewport gate, i.e. when a
|
||||||
|
// pre-rasterization stage routes by gl_ViewportIndex AND the fragment stage can act
|
||||||
|
// on it. The uniform is the honest test for both halves: it exists only where the
|
||||||
|
// gate was injected, and the gate is injected only where a stage routes.
|
||||||
|
Bool RoutesViewportIndex() const { return m_viewportPassMaskUniformLocation >= 0; }
|
||||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||||
// False when the last SyncToBackend could not produce a usable program (a
|
// False when the last SyncToBackend could not produce a usable program (a
|
||||||
@@ -1082,6 +1361,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// qualifier, so the overrides are baked into the source). A mismatch means the
|
// qualifier, so the overrides are baked into the source). A mismatch means the
|
||||||
// program is stale exactly like the clamp masks above.
|
// program is stale exactly like the clamp masks above.
|
||||||
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
||||||
|
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
|
||||||
|
// and the top of the reserved shader-storage range their counter blocks were
|
||||||
|
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
|
||||||
|
// program that uses no atomic counter, which is what keeps the per-draw cost of the
|
||||||
|
// counter sync at one empty-vector test.
|
||||||
|
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
||||||
|
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
||||||
|
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
|
||||||
|
// for, or -1 when this program needed no such stage. Another of the same shape as the
|
||||||
|
// signatures above: the value is compiled INTO the synthesized stage as
|
||||||
|
// `layout(vertices = N) out`, so a program built for one patch size is stale for
|
||||||
|
// another and the draw path has to say so. -1 compares equal to itself for every
|
||||||
|
// program that has a control stage of its own, i.e. for all but a handful.
|
||||||
|
Int GetPassthroughTessControlPatchVertices() const {
|
||||||
|
return m_passthroughTessControlPatchVertices;
|
||||||
|
}
|
||||||
|
|
||||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||||
@@ -1104,10 +1399,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// stale as one built before a relink - while the sampler half, which really is
|
// stale as one built before a relink - while the sampler half, which really is
|
||||||
// re-issued per draw, needs nothing of the sort.
|
// re-issued per draw, needs nothing of the sort.
|
||||||
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
||||||
|
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||||
|
// resolve to are still the ones its ESSL was generated against.
|
||||||
|
//
|
||||||
|
// A fourth condition of the same family as the three above, and the only one that
|
||||||
|
// reads live state rather than a program-side counter, because that is where the
|
||||||
|
// dependency actually is. GLSL ES requires a format layout qualifier on every image
|
||||||
|
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
|
||||||
|
// qualifier is whatever glBindImageTexture named - so a declaration with no format
|
||||||
|
// is compiled against the BINDING, and a rebind to a different format makes the
|
||||||
|
// built program wrong. Keyed on the units the program's own images address (cached
|
||||||
|
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
|
||||||
|
// version above and forces a re-sync anyway), so the cost on a program with no
|
||||||
|
// format-less image - which is all but a handful - is one empty-vector test.
|
||||||
|
//
|
||||||
|
// Deliberately NOT reached from glBindImageTexture: that entry point must never
|
||||||
|
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
|
||||||
|
// state and this comparison notices at the next Prepare, which is also what makes
|
||||||
|
// an image first bound AFTER link work.
|
||||||
|
Bool ImageUnitFormatsStillMatch() const;
|
||||||
|
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
|
||||||
|
// image-unit state. 0 when the program has no format-less image uniform.
|
||||||
|
Uint64 ComputeImageUnitFormatSignature() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
|
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
||||||
|
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
||||||
|
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
|
||||||
|
// stage has been attached and before the link; see the definition for why it cannot
|
||||||
|
// regress a program that works today.
|
||||||
|
void AttachPassthroughTessControlStage(
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
||||||
|
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||||
|
const String& tessEvalStageEssl);
|
||||||
|
|
||||||
|
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||||
|
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||||
|
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||||
|
// input it reads must appear in EsslTranslationKeyInputs - see the definition's
|
||||||
|
// header comment in Managers.cpp and MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||||
|
// False means SPIRV-Cross refused the module; `outError` then carries its message.
|
||||||
|
Bool TranspileSpirvToEssl(const Vector<unsigned int>& spirvCode, GLenum glShaderType,
|
||||||
|
const std::set<String>& xfbCaptureBlockNames,
|
||||||
|
const ImageFormatBakeInputs& imageFormatBake,
|
||||||
|
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||||
|
String& outSource,
|
||||||
|
std::set<String>& outFlattenedXfbBlockNames,
|
||||||
|
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
Uint m_backendProgramId = 0;
|
||||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||||
@@ -1118,6 +1462,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Int m_drawIdUniformLocation = -1;
|
Int m_drawIdUniformLocation = -1;
|
||||||
Int m_baseVertexUniformLocation = -1;
|
Int m_baseVertexUniformLocation = -1;
|
||||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||||
|
Int m_viewportPassMaskUniformLocation = -1;
|
||||||
Int m_indirectParamsBinding = -1;
|
Int m_indirectParamsBinding = -1;
|
||||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||||
@@ -1126,8 +1471,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint m_fragColorBroadcastCount = 1;
|
Uint m_fragColorBroadcastCount = 1;
|
||||||
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
||||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||||
|
Vector<Int> m_atomicCounterGlBindings;
|
||||||
|
Int m_atomicCounterEsslBindingTop = -1;
|
||||||
|
// -1 for every program that has a tessellation control stage of its own (or none at
|
||||||
|
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||||
|
// with. See GetPassthroughTessControlPatchVertices.
|
||||||
|
Int m_passthroughTessControlPatchVertices = -1;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Bool m_backendProgramUsable = false;
|
Bool m_backendProgramUsable = false;
|
||||||
|
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||||
|
// next Use(). Use() dedupes on a GL program NAME, and a relink replaces the
|
||||||
|
// executable behind that name without changing it - see the note at the
|
||||||
|
// glLinkProgram in SyncToBackend for what the driver runs otherwise.
|
||||||
|
Bool m_rebindAfterRelink = false;
|
||||||
|
|
||||||
Int m_globalUboBackendBlockIndex = -1;
|
Int m_globalUboBackendBlockIndex = -1;
|
||||||
Int m_globalUboBackendBlockSize = 0;
|
Int m_globalUboBackendBlockSize = 0;
|
||||||
@@ -1137,6 +1493,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||||
Uint32 m_syncedLinkVersion = ~0u;
|
Uint32 m_syncedLinkVersion = ~0u;
|
||||||
Uint32 m_syncedImageUnitVersion = ~0u;
|
Uint32 m_syncedImageUnitVersion = ~0u;
|
||||||
|
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||||
|
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||||
|
// that declares a format on all of its images, which is the overwhelming majority -
|
||||||
|
// and what keeps the per-draw comparison free for them.
|
||||||
|
Vector<Int> m_formatlessImageUnits;
|
||||||
|
Uint64 m_imageUnitFormatSignature = 0;
|
||||||
SamplerPassMemo m_samplerPassMemo;
|
SamplerPassMemo m_samplerPassMemo;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1180,6 +1542,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// already has costs nothing. 0 when nothing was ever rebound.
|
// already has costs nothing. 0 when nothing was ever rebound.
|
||||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
|
|
||||||
|
// Everything the image-format bake needs from one walk of a program's uniform
|
||||||
|
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||||
|
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
||||||
|
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||||
|
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
||||||
|
// on it.
|
||||||
|
struct ImageFormatBakeInputs {
|
||||||
|
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
||||||
|
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
||||||
|
// one is authoritative and is never overridden.
|
||||||
|
UnorderedMap<String, Uint> glFormatByUniformName;
|
||||||
|
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
||||||
|
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
||||||
|
// write into the emitted declaration instead. Disjoint from the map above by
|
||||||
|
// construction: a format is baked into the module or completed in the text, never
|
||||||
|
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
||||||
|
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
||||||
|
// Units those uniforms address, kept so the draw path can re-read their formats
|
||||||
|
// without walking the reflection again.
|
||||||
|
Vector<Int> units;
|
||||||
|
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
||||||
|
// image uniform, which is all but a handful.
|
||||||
|
Uint64 signature = 0;
|
||||||
|
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
||||||
|
// declaration carries one qualifier, so there is nothing correct to bake and they
|
||||||
|
// are dropped from the map above. Kept for diagnostics.
|
||||||
|
Vector<String> conflictedNames;
|
||||||
|
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||||
|
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||||
|
Bool needsExtendedImageFormats = false;
|
||||||
|
// Some DECLARED format in play is one WidenImageFormatsForEssl will re-declare in a
|
||||||
|
// core carrier. Answered from the uniform reflection rather than from a module parse
|
||||||
|
// on purpose: the widening is armed on every driver, so a per-stage BuildModule to
|
||||||
|
// find out would land on every stage of every program - which is the cost
|
||||||
|
// SpirvGateFeatures exists to avoid. Program-wide, so it can over-arm a stage that
|
||||||
|
// declares no image; the pass then finds nothing, reports no change, and the caller
|
||||||
|
// keeps the module it already had.
|
||||||
|
Bool declaresWidenableImageFormat = false;
|
||||||
|
};
|
||||||
|
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace SamplerImpl {
|
namespace SamplerImpl {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
g_resolvedTier =
|
g_resolvedTier =
|
||||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||||
&g_tierResolution);
|
&g_tierResolution);
|
||||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||||
@@ -267,7 +267,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||||
if (g_announcedTiers & bit) return;
|
if (g_announcedTiers & bit) return;
|
||||||
g_announcedTiers |= bit;
|
g_announcedTiers |= bit;
|
||||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tier this particular batch can actually take. A tier is demoted here when
|
// The tier this particular batch can actually take. A tier is demoted here when
|
||||||
@@ -414,14 +414,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||||
if (batched) {
|
if (batched) {
|
||||||
|
ForEachViewportRoutingPass([&] {
|
||||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||||
drawcount, 0);
|
drawcount, 0);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||||
|
ForEachViewportRoutingPass([&] {
|
||||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
@@ -442,8 +446,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
if (count[i] <= 0) continue;
|
if (count[i] <= 0) continue;
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
|
ForEachViewportRoutingPass([&] {
|
||||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
basevertex ? basevertex[i] : 0);
|
basevertex ? basevertex[i] : 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
@@ -490,7 +496,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||||
subDrawCount, indexSize);
|
subDrawCount, indexSize);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||||
"buffer; skipping the batch",
|
"buffer; skipping the batch",
|
||||||
i);
|
i);
|
||||||
return false;
|
return false;
|
||||||
@@ -515,8 +521,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||||
// application passed for this sub-draw.
|
// application passed for this sub-draw.
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
|
ForEachViewportRoutingPass([&] {
|
||||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||||
|
});
|
||||||
cursor += static_cast<SizeT>(count[i]);
|
cursor += static_cast<SizeT>(count[i]);
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
@@ -596,7 +604,7 @@ void main() {
|
|||||||
|
|
||||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||||
if (shader == 0) {
|
if (shader == 0) {
|
||||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const char* source = kFlattenComputeSource;
|
const char* source = kFlattenComputeSource;
|
||||||
@@ -607,14 +615,14 @@ void main() {
|
|||||||
if (status != GL_TRUE) {
|
if (status != GL_TRUE) {
|
||||||
char log[1024] = {};
|
char log[1024] = {};
|
||||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||||
g_GLESFuncs.glDeleteShader(shader);
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||||
if (program == 0) {
|
if (program == 0) {
|
||||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||||
g_GLESFuncs.glDeleteShader(shader);
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -625,7 +633,7 @@ void main() {
|
|||||||
if (status != GL_TRUE) {
|
if (status != GL_TRUE) {
|
||||||
char log[1024] = {};
|
char log[1024] = {};
|
||||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||||
g_GLESFuncs.glDeleteProgram(program);
|
g_GLESFuncs.glDeleteProgram(program);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -635,7 +643,7 @@ void main() {
|
|||||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||||
g_computeProgramFailed = false;
|
g_computeProgramFailed = false;
|
||||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,7 +878,9 @@ void main() {
|
|||||||
if (flattened.indexCount != 0) {
|
if (flattened.indexCount != 0) {
|
||||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||||
|
ForEachViewportRoutingPass([&] {
|
||||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||||
|
});
|
||||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -920,7 +930,7 @@ void main() {
|
|||||||
feedBaseVertex);
|
feedBaseVertex);
|
||||||
}
|
}
|
||||||
if (!drawn) {
|
if (!drawn) {
|
||||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||||
"the batch was dropped",
|
"the batch was dropped",
|
||||||
drawcount, mode, type);
|
drawcount, mode, type);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
|
|
||||||
|
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
||||||
|
// GLSL ES cannot spell as an image is carried by one it can.
|
||||||
|
//
|
||||||
|
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
||||||
|
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
||||||
|
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
||||||
|
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
||||||
|
// Seventeen have a core format of the SAME per-channel width and component type,
|
||||||
|
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
||||||
|
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
||||||
|
// dropping the components the format does not have, so the carrier's surplus channels
|
||||||
|
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
||||||
|
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
||||||
|
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
||||||
|
//
|
||||||
|
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
||||||
|
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
||||||
|
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
||||||
|
// diagnostic rather than a silent approximation), and on a driver that HAS
|
||||||
|
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
||||||
|
// happen behind it.
|
||||||
|
//
|
||||||
|
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
||||||
|
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
||||||
|
// attachment behaviour, so the image constraint wins. In practice that only bites
|
||||||
|
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
||||||
|
// signed-normalized instead of becoming the half float that fallback would have picked -
|
||||||
|
// so an image-bound texture in one of those two formats is no longer attachable, and
|
||||||
|
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
||||||
|
// does not update. Accepted deliberately: before the widening, an image binding in either
|
||||||
|
// format was refused outright by every driver tested and the stage that declared it never
|
||||||
|
// compiled at all, so nothing that works today is being given up.
|
||||||
|
//
|
||||||
|
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
||||||
|
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
||||||
|
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
||||||
|
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
||||||
|
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
||||||
|
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
||||||
|
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
||||||
|
// from "alpha" to a channel count, which is its own change.
|
||||||
|
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
|
||||||
|
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
|
||||||
|
// shadow already holds SourceChannels components of exactly the carrier's own type, so
|
||||||
|
// padding it out to four is the whole conversion. The packed entries do not - their shadow
|
||||||
|
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
|
||||||
|
// type takes twelve or sixteen bytes out of four and shears the level.
|
||||||
|
enum class ImageWidenSourceEncoding : Uint8 {
|
||||||
|
Components = 0,
|
||||||
|
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
|
||||||
|
PackedFloat11f11f10f,
|
||||||
|
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
|
||||||
|
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
|
||||||
|
// only in what the codes MEAN, which is the shader's business and not the transfer's.
|
||||||
|
PackedInt2101010Rev,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImageBindableStorageWidening {
|
||||||
|
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
||||||
|
GLenum Format = GL_UNKNOWN_MGL;
|
||||||
|
GLenum Type = GL_UNKNOWN_MGL;
|
||||||
|
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
||||||
|
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
||||||
|
Uint SourceChannels = 0;
|
||||||
|
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
||||||
|
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||||
|
// RG8UI), so the carrier decides.
|
||||||
|
Bool IntegerData = false;
|
||||||
|
// What the upload has to do to the frontend shadow before it describes the level to
|
||||||
|
// the driver (PrepareImageWidenedUpload).
|
||||||
|
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
||||||
|
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
||||||
|
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
||||||
|
// has no image format of any width for and which a float carrier would requantise.
|
||||||
|
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
||||||
|
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
||||||
|
//
|
||||||
|
// Two things depend on it, both because the ES storage no longer shares the frontend
|
||||||
|
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
||||||
|
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
||||||
|
// one), and glGetTexImage divides the codes back out into the floats the application
|
||||||
|
// is still owed.
|
||||||
|
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||||
|
Bool SignedNormalized = false;
|
||||||
|
|
||||||
|
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
||||||
|
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||||
|
};
|
||||||
|
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||||
|
|
||||||
|
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
|
||||||
|
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
|
||||||
|
//
|
||||||
|
// A buffer texture cannot be widened: its texels are the application's buffer object, at
|
||||||
|
// the size and layout the application gave it, and it is usually also a vertex, index or
|
||||||
|
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
|
||||||
|
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
|
||||||
|
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
|
||||||
|
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
|
||||||
|
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
|
||||||
|
// nothing.
|
||||||
|
//
|
||||||
|
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
|
||||||
|
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
|
||||||
|
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
|
||||||
|
// reason the storage widening's gaps are - on a driver where the split applies at all
|
||||||
|
// there is no legal ESSL for the image declaration, so such a program did not compile.
|
||||||
|
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||||
@@ -115,6 +224,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
void* pixels, Bool applyPackImageParams);
|
void* pixels, Bool applyPackImageParams);
|
||||||
|
|
||||||
|
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
|
||||||
|
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
|
||||||
|
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
|
||||||
|
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
|
||||||
|
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
|
||||||
|
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
|
||||||
|
// False when `type` is not a 4-byte packed type.
|
||||||
|
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||||
|
GLenum type, void* pixels, Bool applyPackImageParams);
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
@@ -137,10 +256,144 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
||||||
String RetargetTextureBufferExtension(String glslCode,
|
String RetargetTextureBufferExtension(String glslCode,
|
||||||
MG_External::GLESCapabilities::TextureBufferTier tier);
|
MG_External::GLESCapabilities::TextureBufferTier tier);
|
||||||
|
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
|
||||||
|
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
|
||||||
|
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
|
||||||
|
// because only it knows which formats are in play AND whether the driver advertises the
|
||||||
|
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||||
|
// never emitted speculatively. A no-op when not needed or already present.
|
||||||
|
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||||
|
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
|
||||||
|
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
|
||||||
|
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
|
||||||
|
// spelling for it at any version, so the request has to be made here or the stage does
|
||||||
|
// not compile - which loses the whole program, not just the multi-viewport routing.
|
||||||
|
// `needed` is the caller's answer for the same reason as above: only it knows whether the
|
||||||
|
// driver advertises the extension, and requesting an unadvertised one is itself a compile
|
||||||
|
// error, so this is never emitted speculatively. A no-op when not needed or already
|
||||||
|
// present.
|
||||||
|
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
||||||
|
// Writes a format layout qualifier into the image declarations named in
|
||||||
|
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||||
|
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||||
|
// format in, but SPIRV-Cross throws rather than printing the formats it calls
|
||||||
|
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
|
||||||
|
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
|
||||||
|
// stage. So those formats stay out of the module and are spelled here instead, on the
|
||||||
|
// emitted text, where nothing can refuse them.
|
||||||
|
//
|
||||||
|
// Declarations that already carry a format are left exactly as they are, whoever wrote
|
||||||
|
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
|
||||||
|
// stops being safe to edit by hand.
|
||||||
|
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||||
String RemoveLayoutBinding(const String& glslCode);
|
String RemoveLayoutBinding(const String& glslCode);
|
||||||
|
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
||||||
|
// image array into; the suffix is the array's own name and the element's index.
|
||||||
|
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
||||||
|
// One image ARRAY whose elements the application pointed at units that are not
|
||||||
|
// consecutive-from-element-zero.
|
||||||
|
struct ImageArrayUnitPlan {
|
||||||
|
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||||
|
Vector<Int> units; // the frontend image unit element k has to reach
|
||||||
|
};
|
||||||
|
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||||
|
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||||
|
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||||
|
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||||
|
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||||
|
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||||
|
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||||
|
// wrong value and three were never written, with no GL error and no link log. The same
|
||||||
|
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||||
|
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
||||||
|
// INVALID_OPERATION.
|
||||||
|
//
|
||||||
|
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
||||||
|
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
||||||
|
// element k. One declaration carries one binding, so one declaration per unit is the
|
||||||
|
// only spelling that reaches an arbitrary set of them.
|
||||||
|
//
|
||||||
|
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||||
|
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||||
|
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||||
|
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||||
|
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||||
|
// widening the array to cover the whole span of units and routing each subscript through
|
||||||
|
// a `const highp int` offset table - was written before that pass covered images, and
|
||||||
|
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
||||||
|
// The split also costs exactly the image uniforms the application declared, where the
|
||||||
|
// widening cost the whole SPAN (seven for the four elements of
|
||||||
|
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
||||||
|
// fail to fit in.
|
||||||
|
//
|
||||||
|
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||||
|
// caller to report - when the emitted extent disagrees with the reflection, when the
|
||||||
|
// array is reached by anything other than a subscript, or when a subscript is not a
|
||||||
|
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
||||||
|
//
|
||||||
|
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||||
|
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||||
|
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
||||||
|
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
||||||
|
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
||||||
|
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
||||||
|
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||||
|
Vector<String>* outDeclined = nullptr);
|
||||||
|
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||||
|
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||||
|
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||||
|
// `out` one.
|
||||||
|
//
|
||||||
|
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||||
|
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||||
|
// which members, depends on what the application's shader touched; a synthesized stage
|
||||||
|
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||||
|
// program that has no other problem.
|
||||||
|
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||||
|
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||||
|
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||||
|
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||||
|
//
|
||||||
|
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||||
|
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||||
|
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||||
|
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||||
|
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||||
|
// bound in its place, and every draw silently renders nothing.
|
||||||
|
//
|
||||||
|
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||||
|
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||||
|
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||||
|
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||||
|
//
|
||||||
|
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||||
|
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||||
|
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||||
|
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||||
|
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||||
|
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||||
|
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||||
|
//
|
||||||
|
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||||
|
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||||
|
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||||
|
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||||
|
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||||
|
// VkShaderModule against a driver shader object.
|
||||||
|
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||||
|
const String& inPerVertexMembers,
|
||||||
|
const String& outPerVertexMembers);
|
||||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||||
|
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
||||||
|
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
||||||
|
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
||||||
|
// two that use it differently arrive at different ones - which is exactly the property
|
||||||
|
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
||||||
|
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
||||||
|
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
||||||
|
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
||||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||||
@@ -152,15 +405,74 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||||
// up as a device compile failure - and then as a silently no-op draw.
|
// up as a device compile failure - and then as a silently no-op draw.
|
||||||
//
|
//
|
||||||
// Restores a legal declaration:
|
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
||||||
// * loaded only -> add `readonly`
|
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
||||||
// * stored only -> add `writeonly`
|
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||||
// * both -> emit TWO declarations on the same binding and of the
|
// * both -> emit TWO declarations on the same binding and of the
|
||||||
// same type, `readonly <name>` and `writeonly
|
// same type, `coherent readonly
|
||||||
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
|
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
||||||
// imageStore at the second one. Several image variables
|
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
||||||
// may share an image unit as long as they have the same
|
// every imageStore at the second one, and follow each of
|
||||||
// type and format, which is exactly what the pair is.
|
// those stores with `memoryBarrierImage();`. Several image
|
||||||
|
// variables may share an image unit as long as they have
|
||||||
|
// the same type and format, which is exactly what the pair
|
||||||
|
// is.
|
||||||
|
//
|
||||||
|
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
||||||
|
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
||||||
|
// declared in two stages to be declared identically - so a shader that stores an image from
|
||||||
|
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
||||||
|
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
||||||
|
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
||||||
|
// still reads back its initial contents
|
||||||
|
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
||||||
|
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
||||||
|
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
||||||
|
//
|
||||||
|
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
||||||
|
// point: two stages that use an image the same way emit byte-identical declarations, so
|
||||||
|
// letting them keep one shared name costs nothing and merging them is correct, while two
|
||||||
|
// stages that use it differently land on different prefixes and cannot be merged at all.
|
||||||
|
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
||||||
|
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
||||||
|
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
||||||
|
// binding_images_texture_type_* declares three read+write images in each of its five
|
||||||
|
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
||||||
|
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||||
|
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||||
|
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||||
|
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||||
|
// catches this.
|
||||||
|
//
|
||||||
|
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
||||||
|
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
|
||||||
|
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
|
||||||
|
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
|
||||||
|
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
|
||||||
|
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
|
||||||
|
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
|
||||||
|
// nothing but the identifier changed. Stages that agree still reach the same alias and
|
||||||
|
// stay merged, so this costs no shader an extra image uniform.
|
||||||
|
//
|
||||||
|
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
|
||||||
|
// readonly and writeonly (a spelling no access analysis produces, so it came from the
|
||||||
|
// application and is identical everywhere), and one carrying NEITHER, which is legal only
|
||||||
|
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
|
||||||
|
//
|
||||||
|
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||||
|
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||||
|
// one when both are coherent, and the split is what makes a same-variable
|
||||||
|
// read-after-write cross-variable. The single-declaration repairs above do not get it -
|
||||||
|
// nothing aliases them.
|
||||||
|
//
|
||||||
|
// The barrier is the other half of the same problem, and coherent alone did not cover it:
|
||||||
|
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
|
||||||
|
// variable and a read of another it has no reason to believe alias, and is free to serve
|
||||||
|
// the read from before the write - which is what advanced-memory-order's store/load/
|
||||||
|
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
|
||||||
|
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
|
||||||
|
// non-uniform control flow. It costs something in a shader that stores to a read+write
|
||||||
|
// image in a loop, which is why it is confined to the split pair.
|
||||||
//
|
//
|
||||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||||
@@ -170,8 +482,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
//
|
//
|
||||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||||
// already rewritten to and must run before those bindings are stripped - see the call
|
// already rewritten to and must run before those bindings are stripped - see the call
|
||||||
// site in Managers.cpp.
|
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
||||||
String SplitReadWriteImageUniforms(const String& glslCode);
|
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
||||||
|
//
|
||||||
|
// `outSplitCount`, when given, receives the number of declarations that were actually
|
||||||
|
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
||||||
|
// application declared. Zero for every shader but a handful, and the only number the
|
||||||
|
// budget note above can be reported with.
|
||||||
|
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
|
||||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
#include "BackendObject_DirectVulkan.h"
|
#include "BackendObject_DirectVulkan.h"
|
||||||
#include "MG_Backend/BackendObject.h"
|
#include "MG_Backend/BackendObject.h"
|
||||||
#include "DirectVulkan.h"
|
#include "DirectVulkan.h"
|
||||||
|
#include "SubgroupSupportPolicy.h"
|
||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||||
@@ -383,6 +385,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
UpdateAdvertisedExtensions();
|
UpdateAdvertisedExtensions();
|
||||||
|
if (MG_State::pGLContext) {
|
||||||
|
MG_State::pGLContext->InvalidateCompileEnv();
|
||||||
|
}
|
||||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||||
MutableFormatCapabilities());
|
MutableFormatCapabilities());
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -497,30 +502,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.ExtraVendor = Nullopt,
|
.ExtraVendor = Nullopt,
|
||||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||||
.TargetGLSLVersion = {4, 6, 0},
|
.TargetGLSLVersion = {4, 6, 0},
|
||||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false},
|
.IsCompatibilityProfile = false},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||||
return rendererInfo;
|
return rendererInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported) {
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||||
|
E_GL_ARB_multi_draw_indirect,
|
||||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||||
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||||
E_GL_ARB_explicit_attrib_location,
|
E_GL_ARB_explicit_attrib_location,
|
||||||
|
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||||
|
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||||
|
// version: a caller that finds the extension missing never resolves
|
||||||
|
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||||
|
// blocks anyway calls through a null pointer.
|
||||||
|
E_GL_ARB_uniform_buffer_object,
|
||||||
|
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||||
|
// so on a 4.0 context the string is the only way to reach it.
|
||||||
|
E_GL_ARB_stencil_texturing,
|
||||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
E_GL_ARB_get_program_binary};
|
E_GL_ARB_get_program_binary};
|
||||||
|
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||||
|
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||||
|
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||||
|
// be rebased to OpenGL's zero-based semantics.
|
||||||
|
if (nonZeroIndirectBaseInstanceSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_base_instance);
|
||||||
|
}
|
||||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
@@ -539,12 +562,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
}
|
}
|
||||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
|
||||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
|
||||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
|
||||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
// so an application that simply uses doubles needs nothing advertised. What the extension
|
||||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
|
||||||
// make an application that checks the string take a path MobileGL cannot honour.
|
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
|
||||||
|
// decision about all of it rather than about the shader path alone.
|
||||||
if (MG_Config::Features.AdvertiseFp64) {
|
if (MG_Config::Features.AdvertiseFp64) {
|
||||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||||
}
|
}
|
||||||
@@ -669,6 +693,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_vulkanCaps = capabilities;
|
m_vulkanCaps = capabilities;
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
UpdateAdvertisedExtensions();
|
UpdateAdvertisedExtensions();
|
||||||
|
if (MG_State::pGLContext) {
|
||||||
|
MG_State::pGLContext->InvalidateCompileEnv();
|
||||||
|
}
|
||||||
MutableFormatCapabilities().Clear();
|
MutableFormatCapabilities().Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,9 +706,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||||
// the whole list keeps re-runs idempotent.
|
// the whole list keeps re-runs idempotent.
|
||||||
|
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||||
|
// extension by itself on devices with no native subgroup support at all; a
|
||||||
|
// device with native subgroups always advertises - and uses - those.
|
||||||
|
const Bool subgroupSupportAdvertised =
|
||||||
|
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||||
|
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||||
|
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||||
@@ -814,6 +848,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||||
kMaxAdvertisedBufferBlocks);
|
kMaxAdvertisedBufferBlocks);
|
||||||
|
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
|
||||||
|
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
|
||||||
|
// carries), so the stage limits differ only by whether the stage can have blocks at all.
|
||||||
|
//
|
||||||
|
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
|
||||||
|
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
|
||||||
|
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
|
||||||
|
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
|
||||||
|
// through this backend - those write their result through a storage block declared in a
|
||||||
|
// tessellation stage. Gating would report 0 and turn 433 passing cases into
|
||||||
|
// "unsupported", removing function that demonstrably works.
|
||||||
|
//
|
||||||
|
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
|
||||||
|
// the driver refuses outright at link time; the honest limit converts a silent
|
||||||
|
// wrong-render into a capability an application can route around. Here there is no such
|
||||||
|
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
|
||||||
|
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
|
||||||
|
// pipeline creation where the rejection is observable, not on a feature bit this driver
|
||||||
|
// reports inaccurately.
|
||||||
|
{
|
||||||
|
const Int maxPerStageStorageBlocks =
|
||||||
|
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
|
||||||
|
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
||||||
|
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
|
||||||
|
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||||
|
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||||
|
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||||
|
// The one hard capability in the set: no geometry stage means no blocks in it.
|
||||||
|
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
||||||
|
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
|
||||||
|
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||||
|
}
|
||||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||||
@@ -840,8 +906,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||||
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
||||||
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
// Same shape as the image-uniform limits three lines above: maxClipDistances is reported
|
||||||
|
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
|
||||||
|
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
|
||||||
|
// it the limit describes a capacity no shader may use, so report none.
|
||||||
|
m_dynamicParameters.MaxClipDistances =
|
||||||
|
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||||
|
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||||
|
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||||
|
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
|
||||||
|
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
|
||||||
|
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
|
||||||
|
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
|
||||||
|
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
|
||||||
|
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||||
@@ -886,26 +966,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Never, on any device, and no longer for the reason it used to be. It used to track
|
// The device feature the whole fp64 story hangs off. With it, a module keeps its
|
||||||
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
|
// OpCapability Float64 and real doubles reach the driver; without it the transpile
|
||||||
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
|
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
|
||||||
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
|
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
|
||||||
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
|
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
|
||||||
|
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||||
|
// exactly as it always has.
|
||||||
|
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||||
|
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||||
|
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||||
|
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||||
|
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
|
||||||
|
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
|
||||||
|
// bitcast it back.
|
||||||
//
|
//
|
||||||
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
// Re-coupling it does not work, and the reason is worth recording because it is not
|
||||||
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
|
||||||
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
|
||||||
// input would be silent garbage. Reconstructing the value would mean decoding the
|
// `in vec4` is not only legal but the common case
|
||||||
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
|
||||||
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
|
||||||
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
|
||||||
// dvec4 cannot even express within one attribute location.
|
// narrowing OFF for every one of them and the attributes come back unfetched.
|
||||||
//
|
//
|
||||||
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
|
||||||
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
|
||||||
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
|
||||||
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
// ShaderCompiler::SanitizeAndOptimizeBinary.
|
||||||
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
|
||||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||||
@@ -915,6 +1003,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||||
|
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||||
|
// advertised values describe the 32-lane virtual subgroup the compute
|
||||||
|
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||||
|
// GL requires the advertisement and the execution to agree, and on this
|
||||||
|
// path the emulation is what executes; only the compute stage is offered.
|
||||||
|
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
||||||
|
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||||
|
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||||
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||||
|
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||||
|
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||||
} else {
|
} else {
|
||||||
m_dynamicParameters.SubgroupSize = 0;
|
m_dynamicParameters.SubgroupSize = 0;
|
||||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// POST screen shows.
|
// POST screen shows.
|
||||||
|
|
||||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
// capabilities). A live backend copies this in its constructor and
|
||||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||||
// exist; callers that need the advertised list for a known capability set must
|
// exist; callers that need the advertised list for a known capability set must
|
||||||
// use BuildAdvertisedExtensions instead.
|
// use BuildAdvertisedExtensions instead.
|
||||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// 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,
|
||||||
Bool anisotropicFilteringSupported);
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported);
|
||||||
|
|
||||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// slot's ownership unambiguous.
|
// slot's ownership unambiguous.
|
||||||
Uint64 programLifetimeId = 0;
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 backendStateVersion = 0;
|
Uint32 backendStateVersion = 0;
|
||||||
|
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
|
||||||
|
// version, and the pipeline composite is unnamed so the in-place patch in
|
||||||
|
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
|
||||||
|
// mirror replay bumps only the program's block-binding version. Without this
|
||||||
|
// key the composite's slot kept serving the pre-rebind block.binding.
|
||||||
|
Uint32 blockBindingVersion = 0;
|
||||||
Vector<StorageBlockResource> storageBlocks;
|
Vector<StorageBlockResource> storageBlocks;
|
||||||
Vector<BufferVariableResource> bufferVariables;
|
Vector<BufferVariableResource> bufferVariables;
|
||||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||||
@@ -156,18 +162,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||||
|
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
|
||||||
// The lifetime id must match too: a new program that reuses a deleted
|
// The lifetime id must match too: a new program that reuses a deleted
|
||||||
// program's name and happens to land on the same backendStateVersion (both
|
// program's name and happens to land on the same backendStateVersion (both
|
||||||
// count from zero) would otherwise be served the dead program's reflection.
|
// count from zero) would otherwise be served the dead program's reflection.
|
||||||
if (cache.programLifetimeId == programLifetimeId &&
|
if (cache.programLifetimeId == programLifetimeId &&
|
||||||
cache.backendStateVersion == backendStateVersion &&
|
cache.backendStateVersion == backendStateVersion &&
|
||||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||||
|
if (cache.blockBindingVersion != blockBindingVersion) {
|
||||||
|
// Only the block bindings moved (glShaderStorageBlockBinding, or the
|
||||||
|
// pipeline composite's mirror replay - neither touches the backend
|
||||||
|
// state version): the reflection itself is unchanged, so re-apply the
|
||||||
|
// overrides by name instead of re-running spirv-reflect. Overrides
|
||||||
|
// only ever accumulate, so a block without one still holds its
|
||||||
|
// declared binding.
|
||||||
|
for (auto& block : cache.storageBlocks) {
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
|
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||||
|
}
|
||||||
|
cache.blockBindingVersion = blockBindingVersion;
|
||||||
|
}
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = {};
|
cache = {};
|
||||||
cache.programLifetimeId = programLifetimeId;
|
cache.programLifetimeId = programLifetimeId;
|
||||||
cache.backendStateVersion = backendStateVersion;
|
cache.backendStateVersion = backendStateVersion;
|
||||||
|
cache.blockBindingVersion = blockBindingVersion;
|
||||||
|
|
||||||
Vector<SpvReflectShaderModule> modules;
|
Vector<SpvReflectShaderModule> modules;
|
||||||
Vector<Bool> validModules;
|
Vector<Bool> validModules;
|
||||||
@@ -269,14 +290,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
drawBuffer->SyncPersistentMappedRange();
|
drawBuffer->SyncPersistentMappedRange();
|
||||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||||
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return drawBuffer->MappedData() + commandOffset;
|
return drawBuffer->MappedData() + commandOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!indirect) {
|
if (!indirect) {
|
||||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +419,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
stride = sizeof(DrawArraysIndirectCommand);
|
stride = sizeof(DrawArraysIndirectCommand);
|
||||||
}
|
}
|
||||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||||
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||||
stride, sizeof(DrawArraysIndirectCommand));
|
stride, sizeof(DrawArraysIndirectCommand));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -446,20 +467,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
stride = sizeof(DrawArraysIndirectCommand);
|
stride = sizeof(DrawArraysIndirectCommand);
|
||||||
}
|
}
|
||||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||||
stride, sizeof(DrawArraysIndirectCommand));
|
stride, sizeof(DrawArraysIndirectCommand));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parameterBuffer->SyncPersistentMappedRange();
|
parameterBuffer->SyncPersistentMappedRange();
|
||||||
if (parameterBuffer->MappedData() == nullptr) {
|
if (parameterBuffer->MappedData() == nullptr) {
|
||||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +534,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
if (indexSize == 0) {
|
if (indexSize == 0) {
|
||||||
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||||
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||||
}
|
}
|
||||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
const CopyImageEndpoint& dst,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||||
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
|
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||||
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
|
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||||
srcWidth, srcHeight, srcDepth);
|
srcWidth, srcHeight, srcDepth);
|
||||||
}
|
}
|
||||||
void GenerateMipmap(GLenum target) {
|
void GenerateMipmap(GLenum target) {
|
||||||
@@ -1009,7 +1030,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// shift - the hardware divide was the hottest instruction of this loop.
|
// shift - the hardware divide was the hottest instruction of this loop.
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
if (indexSize == 0) {
|
if (indexSize == 0) {
|
||||||
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height);
|
GLsizei height);
|
||||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
const CopyImageEndpoint& dst,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||||
// rather than trade a lost device for a barrier into a closed buffer.
|
// rather than trade a lost device for a barrier into a closed buffer.
|
||||||
if (frame.hasCommandBufferRecorded) {
|
if (frame.hasCommandBufferRecorded) {
|
||||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||||
@@ -259,7 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
||||||
// skipped.
|
// skipped.
|
||||||
if (pipeline == VK_NULL_HANDLE) {
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
|
||||||
|
// assembled and the driver refused is a broken invariant, not an expected failure,
|
||||||
|
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
|
||||||
|
// Log.h ordering fix made MGLOG_E live in INFO builds.
|
||||||
|
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||||
"programHash=0x%llx; not caching the failure",
|
"programHash=0x%llx; not caching the failure",
|
||||||
static_cast<unsigned long long>(hash),
|
static_cast<unsigned long long>(hash),
|
||||||
static_cast<unsigned long long>(payload.programHash));
|
static_cast<unsigned long long>(payload.programHash));
|
||||||
@@ -402,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||||
|
|
||||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
vpci.viewportCount = 1;
|
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||||
vpci.scissorCount = 1;
|
// requires viewportCount == scissorCount whenever both are dynamic
|
||||||
|
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||||
|
// clamped this to the device's multiViewport capability.
|
||||||
|
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||||
|
vpci.scissorCount = vpci.viewportCount;
|
||||||
|
|
||||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||||
raster.polygonMode = payload.polygonMode;
|
raster.polygonMode = payload.polygonMode;
|
||||||
@@ -471,9 +480,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
blend.attachmentCount = payload.colorAttachmentCount;
|
blend.attachmentCount = payload.colorAttachmentCount;
|
||||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
||||||
|
|
||||||
|
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
|
||||||
|
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
|
||||||
|
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
|
||||||
|
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
|
||||||
|
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
|
||||||
|
//
|
||||||
|
// The refusal below is what keeps the half-tessellated shape away from the driver when
|
||||||
|
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
|
||||||
|
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
|
||||||
|
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
|
||||||
|
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||||
|
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||||
|
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||||
|
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||||
|
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||||
|
stagesWithPassthrough = *payload.stages;
|
||||||
|
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||||
|
effectiveStages = &stagesWithPassthrough;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
VkShaderStageFlags stagesPresent = 0;
|
||||||
|
for (const auto& stageInfo : *effectiveStages) {
|
||||||
|
stagesPresent |= stageInfo.stage;
|
||||||
|
}
|
||||||
|
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
|
||||||
|
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
|
||||||
|
if (hasTessControl != hasTessEval) {
|
||||||
|
// Latched, and the latch is the point: a failed creation is deliberately never
|
||||||
|
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
|
||||||
|
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
|
||||||
|
// noise, not a diagnostic. One line names the program; the draws it explains are
|
||||||
|
// all the same draw.
|
||||||
|
static Bool s_warnedHalfTessellatedPipeline = false;
|
||||||
|
if (!s_warnedHalfTessellatedPipeline) {
|
||||||
|
s_warnedHalfTessellatedPipeline = true;
|
||||||
|
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
|
||||||
|
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
|
||||||
|
"patchControlPoints=%u. Its draws are skipped; logged once.",
|
||||||
|
hasTessEval ? "an evaluation" : "a control",
|
||||||
|
hasTessEval ? "control" : "evaluation",
|
||||||
|
static_cast<unsigned long long>(payload.programHash),
|
||||||
|
payload.patchControlPoints);
|
||||||
|
}
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||||
gpi.pStages = payload.stages->data();
|
gpi.pStages = effectiveStages->data();
|
||||||
gpi.pVertexInputState = payload.vertexInputState;
|
gpi.pVertexInputState = payload.vertexInputState;
|
||||||
gpi.pInputAssemblyState = &ia;
|
gpi.pInputAssemblyState = &ia;
|
||||||
gpi.pTessellationState =
|
gpi.pTessellationState =
|
||||||
@@ -490,6 +546,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
||||||
|
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
|
||||||
|
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
|
||||||
|
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
|
||||||
|
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
|
||||||
|
// does not apply: while this is reachable it should keep saying so on every draw.
|
||||||
|
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
|
||||||
|
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
||||||
VkResultToString(result),
|
VkResultToString(result),
|
||||||
@@ -522,8 +585,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||||
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
||||||
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
||||||
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
|
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
|
||||||
// INFO-level builds that CTS actually runs against.
|
// should-never-happen report and must survive in the INFO-level builds that CTS
|
||||||
|
// actually runs against, alongside the MGLOG_F lines above.
|
||||||
if (payload.stageSpirvDigests) {
|
if (payload.stageSpirvDigests) {
|
||||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool primitiveRestartEnable = false;
|
Bool primitiveRestartEnable = false;
|
||||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||||
Uint32 patchControlPoints = 3;
|
Uint32 patchControlPoints = 3;
|
||||||
|
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||||
|
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||||
|
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||||
|
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||||
|
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||||
|
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||||
|
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||||
|
Uint32 viewportCount = 1;
|
||||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||||
@@ -71,6 +79,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool fragmentReplacesDepth = false;
|
Bool fragmentReplacesDepth = false;
|
||||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||||
|
// The tessellation control stage this renderer synthesized for a program that has
|
||||||
|
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
|
||||||
|
// fixed-function pass-through; Vulkan has no such thing and
|
||||||
|
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
|
||||||
|
// pipeline outright). Appended to `stages` at creation. A null module means the
|
||||||
|
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||||
|
// refusal it applies when `stages` itself is half-tessellated.
|
||||||
|
//
|
||||||
|
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||||
|
// of which ComputeHash already mixes in.
|
||||||
|
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||||
|
|||||||
@@ -33,6 +33,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
|
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
|
||||||
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
|
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||||
|
|
||||||
|
// Local size of a compute module, read from OpExecutionMode LocalSize; all-zero
|
||||||
|
// when absent. The compile chain pins SPIR-V 1.3, where a literal local size
|
||||||
|
// always reaches the module as this execution mode (LocalSizeId does not exist
|
||||||
|
// yet).
|
||||||
|
struct ComputeLocalSize {
|
||||||
|
Uint32 x = 0;
|
||||||
|
Uint32 y = 0;
|
||||||
|
Uint32 z = 0;
|
||||||
|
Uint64 Total() const { return static_cast<Uint64>(x) * y * z; }
|
||||||
|
};
|
||||||
|
ComputeLocalSize TryGetComputeLocalSize(const Vector<Uint>& spirv) {
|
||||||
|
constexpr SizeT kHeaderWords = 5;
|
||||||
|
constexpr Uint32 kOpExecutionMode = 16;
|
||||||
|
constexpr Uint32 kModeLocalSize = 17;
|
||||||
|
for (SizeT offset = kHeaderWords; offset < spirv.size();) {
|
||||||
|
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||||
|
const Uint32 opcode = spirv[offset] & 0xffffu;
|
||||||
|
if (wordCount == 0 || offset + wordCount > spirv.size()) break;
|
||||||
|
if (opcode == kOpExecutionMode && wordCount >= 6 && spirv[offset + 2] == kModeLocalSize) {
|
||||||
|
return {spirv[offset + 3], spirv[offset + 4], spirv[offset + 5]};
|
||||||
|
}
|
||||||
|
offset += wordCount;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
struct DescriptorKey {
|
struct DescriptorKey {
|
||||||
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
|
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
|
||||||
String name;
|
String name;
|
||||||
@@ -57,7 +83,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool isMember = false;
|
Bool isMember = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders);
|
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages);
|
||||||
|
|
||||||
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||||
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
|
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||||
@@ -376,12 +402,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
spv_diagnostic diagnostic = nullptr;
|
spv_diagnostic diagnostic = nullptr;
|
||||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||||
if (result != SPV_SUCCESS) {
|
if (result != SPV_SUCCESS) {
|
||||||
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
|
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||||
// the validation switch, MGLOG_E is compiled out (Log.h orders
|
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||||
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
|
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||||
// latch is what a test harness asserts on.
|
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||||
MGLOG_I(
|
MGLOG_E(
|
||||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||||
static_cast<Int>(shaderStage),
|
static_cast<Int>(shaderStage),
|
||||||
programExternalIndex,
|
programExternalIndex,
|
||||||
@@ -588,15 +614,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void ReflectStageInterface(ShaderStage targetStage,
|
void ReflectStageInterface(ShaderStage targetStage,
|
||||||
Bool reflectInputs,
|
Bool reflectInputs,
|
||||||
const Vector<SharedPtr<ShaderObject>>& shaders,
|
const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
StageInterfaceSummary& outSummary,
|
StageInterfaceSummary& outSummary,
|
||||||
Uint programExternalIndex,
|
Uint programExternalIndex,
|
||||||
const char* stageLabel) {
|
const char* stageLabel) {
|
||||||
outSummary.slotSignatures.fill(0);
|
outSummary.slotSignatures.fill(0);
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != targetStage) {
|
if (stages[moduleIndex] != targetStage) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,11 +690,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ValidateRasterizationStageInterface(const Vector<SharedPtr<ShaderObject>>& shaders,
|
void ValidateRasterizationStageInterface(const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
ProgramFactory::VkProgramObject& entry,
|
ProgramFactory::VkProgramObject& entry,
|
||||||
Uint programExternalIndex) {
|
Uint programExternalIndex) {
|
||||||
const ShaderStage producerStage = PickClipFixupStage(shaders);
|
const ShaderStage producerStage = PickClipFixupStage(stages);
|
||||||
entry.rasterizationProducerStage = producerStage;
|
entry.rasterizationProducerStage = producerStage;
|
||||||
entry.producerOutputComponentCount = 0;
|
entry.producerOutputComponentCount = 0;
|
||||||
entry.fragmentInputComponentCount = 0;
|
entry.fragmentInputComponentCount = 0;
|
||||||
@@ -677,8 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool hasFragmentStage = false;
|
Bool hasFragmentStage = false;
|
||||||
for (const auto& shader : shaders) {
|
for (const ShaderStage stage : stages) {
|
||||||
if (shader && shader->GetShaderStage() == ShaderStage::Fragment) {
|
if (stage == ShaderStage::Fragment) {
|
||||||
hasFragmentStage = true;
|
hasFragmentStage = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -689,9 +715,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
StageInterfaceSummary producerOutputs{};
|
StageInterfaceSummary producerOutputs{};
|
||||||
StageInterfaceSummary fragmentInputs{};
|
StageInterfaceSummary fragmentInputs{};
|
||||||
ReflectStageInterface(producerStage, false, shaders, spirv, producerOutputs, programExternalIndex,
|
ReflectStageInterface(producerStage, false, stages, spirv, producerOutputs, programExternalIndex,
|
||||||
"producer");
|
"producer");
|
||||||
ReflectStageInterface(ShaderStage::Fragment, true, shaders, spirv, fragmentInputs, programExternalIndex,
|
ReflectStageInterface(ShaderStage::Fragment, true, stages, spirv, fragmentInputs, programExternalIndex,
|
||||||
"fragment");
|
"fragment");
|
||||||
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
|
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
|
||||||
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
|
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
|
||||||
@@ -1266,7 +1292,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (SizeT i = 1; i < group.offsets.size(); ++i) {
|
for (SizeT i = 1; i < group.offsets.size(); ++i) {
|
||||||
if (group.elementBytes == 0 ||
|
if (group.elementBytes == 0 ||
|
||||||
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
|
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
|
||||||
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
MGLOG_D("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
||||||
"non-contiguous element set; the capture layout will differ from GL's",
|
"non-contiguous element set; the capture layout will differ from GL's",
|
||||||
key.second, key.first);
|
key.second, key.first);
|
||||||
break;
|
break;
|
||||||
@@ -1693,14 +1719,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders) {
|
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages) {
|
||||||
Bool hasGeometry = false;
|
Bool hasGeometry = false;
|
||||||
Bool hasTessEval = false;
|
Bool hasTessEval = false;
|
||||||
Bool hasVertex = false;
|
Bool hasVertex = false;
|
||||||
|
|
||||||
for (const auto& shader : shaders) {
|
for (const ShaderStage stage : stages) {
|
||||||
if (!shader) continue;
|
|
||||||
const auto stage = shader->GetShaderStage();
|
|
||||||
hasGeometry |= (stage == ShaderStage::Geometry);
|
hasGeometry |= (stage == ShaderStage::Geometry);
|
||||||
hasTessEval |= (stage == ShaderStage::TessEval);
|
hasTessEval |= (stage == ShaderStage::TessEval);
|
||||||
hasVertex |= (stage == ShaderStage::Vertex);
|
hasVertex |= (stage == ShaderStage::Vertex);
|
||||||
@@ -1721,6 +1745,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
return ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
||||||
case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
|
case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
|
||||||
return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer;
|
return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer;
|
||||||
|
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
|
||||||
|
return ProgramFactory::DescriptorBindingKind::StorageTexelBuffer;
|
||||||
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER:
|
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER:
|
||||||
return ProgramFactory::DescriptorBindingKind::StorageBuffer;
|
return ProgramFactory::DescriptorBindingKind::StorageBuffer;
|
||||||
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE:
|
case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE:
|
||||||
@@ -1750,6 +1776,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler ||
|
if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler ||
|
||||||
kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer ||
|
kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer ||
|
||||||
|
kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer ||
|
||||||
kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||||
const auto arraySuffix = name.find("[0]");
|
const auto arraySuffix = name.find("[0]");
|
||||||
if (arraySuffix != String::npos) {
|
if (arraySuffix != String::npos) {
|
||||||
@@ -1839,8 +1866,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// UniformManager::BindProgramUniformBuffers: UBO instance arrays
|
// UniformManager::BindProgramUniformBuffers: UBO instance arrays
|
||||||
// (uniform Block {...} b[N];), storage-block instance arrays, image uniform
|
// (uniform Block {...} b[N];), storage-block instance arrays, image uniform
|
||||||
// arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
|
// arrays, and combined-image-sampler arrays (uniform sampler2D s[N];).
|
||||||
// Anything else - a uniform TEXEL buffer array is the one remaining kind -
|
// Anything else - the two TEXEL buffer kinds are what remain, samplerBuffer[N]
|
||||||
// must fail program creation cleanly rather than continue with corrupt state.
|
// and imageBuffer[N] - must fail program creation cleanly rather than continue
|
||||||
|
// with corrupt state. Their per-draw path writes pTexelBufferView as the
|
||||||
|
// address of a vector element sized for one descriptor per binding, so an
|
||||||
|
// array would not merely be unresolved, it would dangle.
|
||||||
//
|
//
|
||||||
// Getting listed here is not cosmetic: a kind that is rejected leaves
|
// Getting listed here is not cosmetic: a kind that is rejected leaves
|
||||||
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
|
// GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and
|
||||||
@@ -1849,16 +1879,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// unification and the set->0 normalisation this function exists to do. A
|
// unification and the set->0 normalisation this function exists to do. A
|
||||||
// program with an image array plus any second descriptor got aliased
|
// program with an image array plus any second descriptor got aliased
|
||||||
// bindings out of that, and a DEBUG build trapped on the same program.
|
// bindings out of that, and a DEBUG build trapped on the same program.
|
||||||
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
|
// The refusal below is MGLOG_E and per-program-compile, so it reports every
|
||||||
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
|
// program it declines. It spent time at MGLOG_I because the old level
|
||||||
// in the builds that ship.
|
// ordering compiled E out of the builds that ship.
|
||||||
const Bool arraySupportedForKind =
|
const Bool arraySupportedForKind =
|
||||||
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
|
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
|
||||||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
|
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
|
||||||
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
|
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
|
||||||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
||||||
if (binding->count != 1 && !arraySupportedForKind) {
|
if (binding->count != 1 && !arraySupportedForKind) {
|
||||||
MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||||
"kind (name='%s' count=%u type=%d)",
|
"kind (name='%s' count=%u type=%d)",
|
||||||
binding->name ? binding->name : "<null>", binding->count,
|
binding->name ? binding->name : "<null>", binding->count,
|
||||||
static_cast<Int>(binding->descriptor_type));
|
static_cast<Int>(binding->descriptor_type));
|
||||||
@@ -1991,6 +2021,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
|
||||||
|
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
|
||||||
|
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
|
||||||
|
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||||
|
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||||
|
SpvBuiltIn builtin) {
|
||||||
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
|
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||||
|
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
|
||||||
|
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
|
||||||
|
if (variable != nullptr &&
|
||||||
|
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||||
|
variable->built_in == builtin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||||
SpvBuiltIn builtin) {
|
SpvBuiltIn builtin) {
|
||||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
@@ -2039,7 +2092,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||||
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||||
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2.
|
||||||
|
// This value becomes the storage image VIEW's format while the image itself was created
|
||||||
|
// from the texture's internal format, so the two must name the same bit layout or the
|
||||||
|
// shader reads the texel through a different component order than the host wrote it.
|
||||||
|
// GL_RGB10_A2 with GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, G in 10-19, B in
|
||||||
|
// 20-29 and A in 30-31, which is Vulkan's A2B10G10R10; A2R10G10B10 transposes R and B.
|
||||||
|
// KHR-GL43.shader_image_load_store.basic-allFormats-store read back [2,1,0,3] for an
|
||||||
|
// rgb10_a2ui image stored as [0,1,2,3] while these two converters disagreed.
|
||||||
|
case SpvImageFormatRgb10A2: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||||
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||||
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||||
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||||
@@ -2062,7 +2123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||||
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||||
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2B10G10R10_UINT_PACK32; // see Rgb10A2 above
|
||||||
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||||
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||||
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||||
@@ -2253,15 +2314,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
void ProgramFactory::ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeVertexInputLocationMask = 0;
|
entry.activeVertexInputLocationMask = 0;
|
||||||
entry.vertexInputTypes.fill(0);
|
entry.vertexInputTypes.fill(0);
|
||||||
entry.readsBaseVertexBuiltin = false;
|
entry.readsBaseVertexBuiltin = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
if (stages[moduleIndex] != ShaderStage::Vertex) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2333,15 +2394,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
|
||||||
|
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
|
||||||
|
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||||
|
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||||
|
// pipeline, and a false positive costs only a wider viewportCount.
|
||||||
|
void ProgramFactory::ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||||
|
const Vector<Vector<Uint>>& spirv,
|
||||||
|
VkProgramObject& entry) const {
|
||||||
|
entry.writesViewportIndexBuiltin = false;
|
||||||
|
|
||||||
|
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
|
const ShaderStage stage = stages[moduleIndex];
|
||||||
|
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||||
|
|
||||||
|
const auto& module = spirv[moduleIndex];
|
||||||
|
if (module.empty()) continue;
|
||||||
|
|
||||||
|
SpvReflectShaderModule reflectModule{};
|
||||||
|
const SpvReflectResult createResult =
|
||||||
|
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||||
|
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||||
|
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
|
||||||
|
// silently collapse every viewport onto 0 (the exact bug this reflection exists
|
||||||
|
// to fix); over-declaring costs one extra viewport slot on a program that never
|
||||||
|
// uses it.
|
||||||
|
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
|
||||||
|
"program writes gl_ViewportIndex",
|
||||||
|
static_cast<Int>(createResult));
|
||||||
|
entry.writesViewportIndexBuiltin = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
|
||||||
|
entry.writesViewportIndexBuiltin = true;
|
||||||
|
}
|
||||||
|
spvReflectDestroyShaderModule(&reflectModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramFactory::ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeFragmentOutputLocationMask = 0;
|
entry.activeFragmentOutputLocationMask = 0;
|
||||||
entry.fragmentOutputTypes.fill(0);
|
entry.fragmentOutputTypes.fill(0);
|
||||||
entry.fragmentReplacesDepth = false;
|
entry.fragmentReplacesDepth = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
if (stages[moduleIndex] != ShaderStage::Fragment) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2462,7 +2562,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// inert; a device whose binding cap is smaller than a shader's array is not a
|
// inert; a device whose binding cap is smaller than a shader's array is not a
|
||||||
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
|
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
|
||||||
// reach (256 on desktop, ~16 on mobile).
|
// reach (256 on desktop, ~16 on mobile).
|
||||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
||||||
"this device can describe - declining the program",
|
"this device can describe - declining the program",
|
||||||
kindLabel, uniformName.c_str(), binding, count, maxBindings);
|
kindLabel, uniformName.c_str(), binding, count, maxBindings);
|
||||||
outDeclined = true;
|
outDeclined = true;
|
||||||
@@ -2470,7 +2570,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
if (baseLocation < 0 ||
|
if (baseLocation < 0 ||
|
||||||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
|
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
|
||||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
||||||
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
|
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
|
||||||
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
|
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
|
||||||
"neighbouring uniform",
|
"neighbouring uniform",
|
||||||
@@ -2661,6 +2761,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type);
|
const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type);
|
||||||
if (descriptorKind != DescriptorBindingKind::CombinedImageSampler &&
|
if (descriptorKind != DescriptorBindingKind::CombinedImageSampler &&
|
||||||
descriptorKind != DescriptorBindingKind::UniformTexelBuffer &&
|
descriptorKind != DescriptorBindingKind::UniformTexelBuffer &&
|
||||||
|
descriptorKind != DescriptorBindingKind::StorageTexelBuffer &&
|
||||||
descriptorKind != DescriptorBindingKind::StorageImage &&
|
descriptorKind != DescriptorBindingKind::StorageImage &&
|
||||||
descriptorKind != DescriptorBindingKind::StorageBuffer) {
|
descriptorKind != DescriptorBindingKind::StorageBuffer) {
|
||||||
continue;
|
continue;
|
||||||
@@ -2706,7 +2807,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// a Uint16 on the way, where 65536 would silently become 0.
|
// a Uint16 on the way, where 65536 would silently become 0.
|
||||||
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
|
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
|
||||||
if (storageArrayCount > m_maxBindings) {
|
if (storageArrayCount > m_maxBindings) {
|
||||||
MGLOG_I("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
||||||
"elements, past the %u this device can describe - declining the program",
|
"elements, past the %u this device can describe - declining the program",
|
||||||
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
|
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
|
||||||
entry.declinedDescriptors = true;
|
entry.declinedDescriptors = true;
|
||||||
@@ -2729,7 +2830,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// so at a level that survives a release build, because dropping the binding
|
// so at a level that survives a release build, because dropping the binding
|
||||||
// leaves the shader reading a descriptor the layout never declared.
|
// leaves the shader reading a descriptor the layout never declared.
|
||||||
if (sampler->count > 1) {
|
if (sampler->count > 1) {
|
||||||
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
||||||
"descriptor array with no frontend uniform location (a multi-dimensional array "
|
"descriptor array with no frontend uniform location (a multi-dimensional array "
|
||||||
"of samplers or images is the known cause)",
|
"of samplers or images is the known cause)",
|
||||||
uniformName.c_str(), binding, sampler->count);
|
uniformName.c_str(), binding, sampler->count);
|
||||||
@@ -2790,6 +2891,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (descriptorKind == DescriptorBindingKind::StorageTexelBuffer) {
|
||||||
|
// Only the declared format is recorded, and only so the per-draw resolve can
|
||||||
|
// prefer it over the one glBindImageTexture named. Everything the StorageImage
|
||||||
|
// branch above does about ARRAYS is deliberately absent: an imageBuffer array
|
||||||
|
// is refused outright by the array gate in RemapDescriptorBindingsForVulkan,
|
||||||
|
// exactly as a samplerBuffer array is, so bindingDescriptorCounts stays at the
|
||||||
|
// default 1 and the descriptor write below may take the address of a vector
|
||||||
|
// element without reserving room for extra elements.
|
||||||
|
const VkFormat reflectedFormat =
|
||||||
|
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
|
||||||
|
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
|
||||||
|
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
reflectedFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
existingFormat == reflectedFormat,
|
||||||
|
"ProgramFactory::ReflectLayout: storage texel buffer binding %u ('%s') "
|
||||||
|
"has conflicting reflected formats (%d vs %d)",
|
||||||
|
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
|
||||||
|
static_cast<Int>(reflectedFormat));
|
||||||
|
if (existingFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
existingFormat = reflectedFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
|
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
|
||||||
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
||||||
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
||||||
@@ -2867,6 +2991,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.dynamicBindings.push_back(binding);
|
entry.dynamicBindings.push_back(binding);
|
||||||
} else if (kind == DescriptorBindingKind::UniformTexelBuffer) {
|
} else if (kind == DescriptorBindingKind::UniformTexelBuffer) {
|
||||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||||
|
} else if (kind == DescriptorBindingKind::StorageTexelBuffer) {
|
||||||
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||||
} else if (kind == DescriptorBindingKind::StorageBuffer) {
|
} else if (kind == DescriptorBindingKind::StorageBuffer) {
|
||||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
} else if (kind == DescriptorBindingKind::StorageImage) {
|
} else if (kind == DescriptorBindingKind::StorageImage) {
|
||||||
@@ -2878,8 +3004,73 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bindings.push_back(layoutBinding);
|
bindings.push_back(layoutBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATE_AFTER_BIND is strictly an optional per-layout acceleration. The GL
|
||||||
|
// descriptor model still resolves every sampler uniform element independently
|
||||||
|
// (including its texture-unit sampler-object override); selecting this path
|
||||||
|
// changes neither that resolution nor the set versioning in UniformManager.
|
||||||
|
// A conservative count keeps a layout on ordinary descriptors whenever any
|
||||||
|
// relevant update-after-bind limit is not large enough, rather than asking a
|
||||||
|
// driver to reject it during vkCreateDescriptorSetLayout.
|
||||||
|
Uint32 updateAfterBindSamplers = 0;
|
||||||
|
Uint32 updateAfterBindUniformBuffers = 0;
|
||||||
|
Uint32 updateAfterBindStorageBuffers = 0;
|
||||||
|
Uint32 updateAfterBindSampledImages = 0;
|
||||||
|
Uint32 updateAfterBindStorageImages = 0;
|
||||||
|
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
|
||||||
|
const Uint32 count = entry.bindingDescriptorCounts[binding];
|
||||||
|
switch (entry.bindingKinds[binding]) {
|
||||||
|
case DescriptorBindingKind::UniformBufferDynamic:
|
||||||
|
updateAfterBindUniformBuffers += count;
|
||||||
|
break;
|
||||||
|
case DescriptorBindingKind::CombinedImageSampler:
|
||||||
|
updateAfterBindSamplers += count;
|
||||||
|
updateAfterBindSampledImages += count;
|
||||||
|
break;
|
||||||
|
case DescriptorBindingKind::UniformTexelBuffer:
|
||||||
|
updateAfterBindSampledImages += count;
|
||||||
|
break;
|
||||||
|
case DescriptorBindingKind::StorageBuffer:
|
||||||
|
case DescriptorBindingKind::StorageTexelBuffer:
|
||||||
|
updateAfterBindStorageBuffers += count;
|
||||||
|
break;
|
||||||
|
case DescriptorBindingKind::StorageImage:
|
||||||
|
updateAfterBindStorageImages += count;
|
||||||
|
break;
|
||||||
|
case DescriptorBindingKind::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const Uint32 updateAfterBindResources = updateAfterBindUniformBuffers + updateAfterBindStorageBuffers +
|
||||||
|
updateAfterBindSampledImages + updateAfterBindStorageImages;
|
||||||
|
const auto& uab = m_updateAfterBindLimits;
|
||||||
|
entry.usesUpdateAfterBind =
|
||||||
|
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||||
|
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||||
|
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||||
|
updateAfterBindSampledImages <= uab.maxPerStageSampledImages &&
|
||||||
|
updateAfterBindStorageImages <= uab.maxPerStageStorageImages &&
|
||||||
|
updateAfterBindResources <= uab.maxPerStageResources &&
|
||||||
|
updateAfterBindSamplers <= uab.maxSetSamplers &&
|
||||||
|
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffers &&
|
||||||
|
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffersDynamic &&
|
||||||
|
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffers &&
|
||||||
|
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffersDynamic &&
|
||||||
|
updateAfterBindSampledImages <= uab.maxSetSampledImages &&
|
||||||
|
updateAfterBindStorageImages <= uab.maxSetStorageImages;
|
||||||
|
|
||||||
|
Vector<VkDescriptorBindingFlags> bindingFlags;
|
||||||
|
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{};
|
||||||
|
if (entry.usesUpdateAfterBind) {
|
||||||
|
bindingFlags.assign(bindings.size(), VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT);
|
||||||
|
bindingFlagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
|
||||||
|
bindingFlagsInfo.bindingCount = static_cast<Uint32>(bindingFlags.size());
|
||||||
|
bindingFlagsInfo.pBindingFlags = bindingFlags.data();
|
||||||
|
}
|
||||||
|
|
||||||
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
|
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
|
||||||
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||||
|
setLayoutInfo.flags = entry.usesUpdateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
|
||||||
|
setLayoutInfo.pNext = entry.usesUpdateAfterBind ? &bindingFlagsInfo : nullptr;
|
||||||
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
|
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
|
||||||
setLayoutInfo.pBindings = bindings.data();
|
setLayoutInfo.pBindings = bindings.data();
|
||||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout),
|
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout),
|
||||||
@@ -2956,18 +3147,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrame = m_frameCounter;
|
entry.lastUsedFrame = m_frameCounter;
|
||||||
auto& shaders = program.GetAttachedShaders();
|
// The EXECUTABLE's stage list, not GetAttachedShaders(): `spirv` is a link artifact with
|
||||||
|
// one module per linked stage, while the attach list is live and grows on
|
||||||
|
// glAttachShader, which GL 4.6 core 7.3 says does not reach the executable until the
|
||||||
|
// next link. Sizing this loop by the attach list therefore ran it past the end of both
|
||||||
|
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||||
|
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||||
auto& spirv = program.GetGeneratedSpirv();
|
auto& spirv = program.GetGeneratedSpirv();
|
||||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||||
|
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||||
|
if (enableSpirvValidation) {
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||||
|
}
|
||||||
|
|
||||||
const ShaderStage fixupStage = PickClipFixupStage(shaders);
|
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
||||||
|
|
||||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
// Both lists come from the same Link(), so they agree by construction; the min() is what
|
||||||
|
// makes that an assumption this loop does not have to bet the process on.
|
||||||
|
const SizeT moduleCount = std::min(stages.size(), spirv.size());
|
||||||
|
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||||
auto& spv = spirv[i];
|
auto& spv = spirv[i];
|
||||||
if (spv.empty()) continue;
|
if (spv.empty()) continue;
|
||||||
|
|
||||||
// Apply position fixup if needed
|
// Apply position fixup if needed
|
||||||
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
if (fixupStage != ShaderStage::Unknown && stages[i] == fixupStage) {
|
||||||
const Vector<Uint>* fixupInput = &spv;
|
const Vector<Uint>* fixupInput = &spv;
|
||||||
Vector<Uint> xfbSpirv;
|
Vector<Uint> xfbSpirv;
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||||
@@ -2983,28 +3186,95 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
moduleSpirvs[i] = spv;
|
moduleSpirvs[i] = spv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
|
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && stages[i] == ShaderStage::Fragment) {
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> explicitLodSpirv;
|
Vector<Uint> explicitLodSpirv;
|
||||||
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
||||||
moduleSpirvs[i] = Move(explicitLodSpirv);
|
moduleSpirvs[i] = Move(explicitLodSpirv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] &&
|
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> fragCoordSpirv;
|
Vector<Uint> fragCoordSpirv;
|
||||||
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
||||||
moduleSpirvs[i] = Move(fragCoordSpirv);
|
moduleSpirvs[i] = Move(fragCoordSpirv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
||||||
|
// operations execute natively; module repairs keep the GL contract intact
|
||||||
|
// around them. The opt-in emulation path replaces them only on devices with no
|
||||||
|
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||||
|
if (stages[i] == ShaderStage::Compute) {
|
||||||
|
// Program 203 broadcasts the first reduction through
|
||||||
|
// prefixSumCache[0], then lets the second reduction overwrite that
|
||||||
|
// scratch without first rendezvousing all readers. Patch that exact
|
||||||
|
// fingerprint before either native or emulated subgroup lowering.
|
||||||
|
if (m_subgroupPolicy.fixIterationRPBarrier) {
|
||||||
|
Vector<Uint> patchedSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
|
||||||
|
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
|
||||||
|
moduleSpirvs[i] = std::move(patchedSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
|
||||||
|
"Program 203 keeps its shared-scratch race",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_subgroupPolicy.emulateSubgroups) {
|
||||||
|
Vector<Uint> emulatedSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
|
||||||
|
moduleSpirvs[i], emulatedSpirv,
|
||||||
|
m_subgroupPolicy.maxComputeSharedMemoryBytes, enableSpirvValidation)) {
|
||||||
|
moduleSpirvs[i] = std::move(emulatedSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: subgroup emulation failed for program %u; the "
|
||||||
|
"module keeps subgroup operations the device cannot execute",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// iterationRP under-declares its cross-subgroup scratch
|
||||||
|
// (prefixSumCache[32] for 512 invocations); on a sub-16-lane device
|
||||||
|
// grow that one fingerprinted array to what the topology needs.
|
||||||
|
if (m_subgroupPolicy.fixIterationRPSubgroupScratch) {
|
||||||
|
Vector<Uint> patchedSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||||
|
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
|
||||||
|
m_subgroupPolicy.maxComputeSharedMemoryBytes,
|
||||||
|
enableSpirvValidation)) {
|
||||||
|
moduleSpirvs[i] = std::move(patchedSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: iterationRP subgroup scratch patch failed for "
|
||||||
|
"program %u; the pack's declared array sizes stay in effect",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// gl_NumSubgroups must agree with the gl_SubgroupID range GL promises;
|
||||||
|
// derive it from the workgroup dimensions and gl_SubgroupSize instead of
|
||||||
|
// trusting a driver builtin that can disagree with the topology the same
|
||||||
|
// dispatch emits (Adreno reports 1 while emitting IDs 0..7 for a
|
||||||
|
// 512-invocation, 64-wide workgroup). The ceil() partition this derives
|
||||||
|
// is pinned by REQUIRE_FULL_SUBGROUPS at pipeline creation whenever the
|
||||||
|
// workgroup shape makes that flag legal (see the stage setup below).
|
||||||
|
if (m_subgroupPolicy.deriveNumSubgroups) {
|
||||||
|
Vector<Uint> derivedNumSubgroupsSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
|
||||||
|
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
|
||||||
|
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
|
||||||
|
"compute shaders may observe a driver-inconsistent subgroup count",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||||
// stored as - which addresses [0,1] where the application addressed texels.
|
// stored as - which addresses [0,1] where the application addressed texels.
|
||||||
{
|
{
|
||||||
Vector<Uint> rectLoweredSpirv;
|
Vector<Uint> rectLoweredSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv, enableSpirvValidation) &&
|
||||||
!rectLoweredSpirv.empty()) {
|
!rectLoweredSpirv.empty()) {
|
||||||
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
||||||
}
|
}
|
||||||
@@ -3017,7 +3287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
{
|
{
|
||||||
Vector<Uint> invariantSpirv;
|
Vector<Uint> invariantSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||||
moduleSpirvs[i], invariantSpirv)) {
|
moduleSpirvs[i], invariantSpirv, enableSpirvValidation)) {
|
||||||
moduleSpirvs[i] = std::move(invariantSpirv);
|
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||||
} else {
|
} else {
|
||||||
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
||||||
@@ -3037,11 +3307,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
||||||
// the builtin but shaderDrawParameters is missing) rides along with
|
// the builtin but shaderDrawParameters is missing) rides along with
|
||||||
// ReflectVertexInputs, which already reflects this stage.
|
// ReflectVertexInputs, which already reflects this stage.
|
||||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
if (stages[i] == ShaderStage::Vertex && m_shaderDrawParametersEnabled) {
|
||||||
m_shaderDrawParametersEnabled) {
|
|
||||||
Vector<Uint> rebasedSpirv;
|
Vector<Uint> rebasedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||||
rebasedSpirv)) {
|
rebasedSpirv, enableSpirvValidation)) {
|
||||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||||
} else {
|
} else {
|
||||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||||
@@ -3055,11 +3324,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||||
// and for the baseVertex word of an indexed indirect command.
|
// and for the baseVertex word of an indexed indirect command.
|
||||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
if (stages[i] == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
|
||||||
Vector<Uint> zeroedSpirv;
|
Vector<Uint> zeroedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||||
zeroedSpirv)) {
|
zeroedSpirv, enableSpirvValidation)) {
|
||||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||||
} else {
|
} else {
|
||||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||||
@@ -3079,10 +3347,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||||
// double input - garbage with no diagnostic anywhere.
|
// double input - garbage with no diagnostic anywhere.
|
||||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
if (stages[i] == ShaderStage::Vertex) {
|
||||||
Vector<Uint> packedSpirv;
|
Vector<Uint> packedSpirv;
|
||||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||||
moduleSpirvs[i], packedSpirv);
|
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||||
MOBILEGL_ASSERT(packOk,
|
MOBILEGL_ASSERT(packOk,
|
||||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||||
"vertex-input format and the shader input type now disagree",
|
"vertex-input format and the shader input type now disagree",
|
||||||
@@ -3106,7 +3374,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_unformattedFloatStorageImagesEnabled) {
|
if (m_unformattedFloatStorageImagesEnabled) {
|
||||||
Vector<Uint> unformattedSpirv;
|
Vector<Uint> unformattedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||||
moduleSpirvs[i], unformattedSpirv)) {
|
moduleSpirvs[i], unformattedSpirv, enableSpirvValidation)) {
|
||||||
moduleSpirvs[i] = std::move(unformattedSpirv);
|
moduleSpirvs[i] = std::move(unformattedSpirv);
|
||||||
} else {
|
} else {
|
||||||
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
||||||
@@ -3118,17 +3386,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||||
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
||||||
|
|
||||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||||
auto& moduleSpv = moduleSpirvs[i];
|
auto& moduleSpv = moduleSpirvs[i];
|
||||||
if (moduleSpv.empty()) continue;
|
if (moduleSpv.empty()) continue;
|
||||||
|
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||||
#else
|
#else
|
||||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||||
// lanes, where the DEBUG gate above is compiled out.
|
// lanes, where the DEBUG gate above is compiled out.
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
if (enableSpirvValidation) {
|
||||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -3140,10 +3408,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
|
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
|
||||||
|
|
||||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||||
ShaderStage shaderStage = shaders[i]->GetShaderStage();
|
ShaderStage shaderStage = stages[i];
|
||||||
stage.stage = ToVkStage(shaderStage);
|
stage.stage = ToVkStage(shaderStage);
|
||||||
stage.module = module;
|
stage.module = module;
|
||||||
stage.pName = "main";
|
stage.pName = "main";
|
||||||
|
// Pin the full-subgroup launch the derived gl_NumSubgroups assumes. Legal
|
||||||
|
// exactly when the computeFullSubgroups feature is enabled and local_size_x is
|
||||||
|
// a multiple of the subgroup size (VUID-VkPipelineShaderStageCreateInfo-
|
||||||
|
// flags-02759/-02785), and only worth requesting while the resulting subgroup
|
||||||
|
// count fits the device's maxComputeWorkgroupSubgroups (lavapipe caps it at
|
||||||
|
// 32, below a 512-invocation dispatch's 64). With the bit set, "Full
|
||||||
|
// Subgroups" guarantees every subgroup launches with all invocations active,
|
||||||
|
// making the subgroup count exactly invocations / size. Shapes the flag
|
||||||
|
// cannot cover (e.g. 32x16 on a 64-wide device) fall back to the driver's
|
||||||
|
// own - spec-encouraged - tight partitioning, which the DriverPost witness
|
||||||
|
// verifies per device.
|
||||||
|
if (shaderStage == ShaderStage::Compute && m_subgroupPolicy.requireFullSubgroups &&
|
||||||
|
!m_subgroupPolicy.emulateSubgroups && m_subgroupPolicy.nativeSubgroupSize != 0) {
|
||||||
|
const ComputeLocalSize localSize = TryGetComputeLocalSize(moduleSpv);
|
||||||
|
const Uint64 fullSubgroupCount =
|
||||||
|
localSize.Total() / m_subgroupPolicy.nativeSubgroupSize;
|
||||||
|
if (localSize.x != 0 && localSize.x % m_subgroupPolicy.nativeSubgroupSize == 0 &&
|
||||||
|
fullSubgroupCount <= m_subgroupPolicy.maxComputeWorkgroupSubgroups) {
|
||||||
|
stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entry.modules.push_back(module);
|
entry.modules.push_back(module);
|
||||||
entry.stages.push_back(stage);
|
entry.stages.push_back(stage);
|
||||||
@@ -3154,10 +3443,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
// Reflect and create layout as part of the program object
|
// Reflect and create layout as part of the program object
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
ValidateRasterizationStageInterface(stages, moduleSpirvs, entry, program.GetExternalIndex());
|
||||||
#endif
|
#endif
|
||||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
ReflectVertexInputs(stages, moduleSpirvs, entry);
|
||||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
ReflectViewportIndexUsage(stages, moduleSpirvs, entry);
|
||||||
|
ReflectFragmentOutputs(stages, moduleSpirvs, entry);
|
||||||
|
ReflectPassthroughTessControlNeed(stages, moduleSpirvs, entry);
|
||||||
ReflectLayout(program, moduleSpirvs, entry);
|
ReflectLayout(program, moduleSpirvs, entry);
|
||||||
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
||||||
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
||||||
@@ -3168,7 +3459,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
|
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
|
||||||
// which clears the flag.
|
// which clears the flag.
|
||||||
if (!remapOk) {
|
if (!remapOk) {
|
||||||
MGLOG_I("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
||||||
"be remapped, so the layout does not describe what the shader reads",
|
"be remapped, so the layout does not describe what the shader reads",
|
||||||
program.GetExternalIndex());
|
program.GetExternalIndex());
|
||||||
entry.declinedDescriptors = true;
|
entry.declinedDescriptors = true;
|
||||||
@@ -3202,17 +3493,248 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
||||||
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
||||||
static_cast<unsigned long long>(hash));
|
static_cast<unsigned long long>(hash));
|
||||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
// The observer destroys dependent pipelines and frees descriptor sets while
|
||||||
// so an observer never observes a half-destroyed entry through a lookup.
|
// this entry still owns its layout. Vulkan requires every descriptor set to be
|
||||||
// Observers only need the handle values to purge their keyed caches.
|
// freed before its VkDescriptorSetLayout is destroyed.
|
||||||
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
if (m_evictionObserver != nullptr) {
|
if (m_evictionObserver != nullptr) {
|
||||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||||
}
|
}
|
||||||
|
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||||
|
it = m_cache.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProgramFactory::~ProgramFactory() {
|
||||||
|
for (auto& entry : m_passthroughTessControlStages) {
|
||||||
|
if (entry.second.module != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
|
||||||
|
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
|
||||||
|
// control shader: "the input patch is passed through unmodified", the output patch has
|
||||||
|
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
|
||||||
|
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||||
|
//
|
||||||
|
// Those two levels default to 1.0 and are baked here as literals because
|
||||||
|
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
|
||||||
|
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
|
||||||
|
// making the levels a parameter of this source AND of the cache key in
|
||||||
|
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
|
||||||
|
// together here.
|
||||||
|
//
|
||||||
|
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
|
||||||
|
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
|
||||||
|
// exactly the built-ins that stage used, and its user-defined inputs (if any) come
|
||||||
|
// straight off the vertex stage's outputs - which a control stage sitting in between
|
||||||
|
// would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather
|
||||||
|
// than let this write a partial interface.
|
||||||
|
//
|
||||||
|
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||||
|
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||||
|
// this from having to know the domain.
|
||||||
|
String source = "#version 450 core\n";
|
||||||
|
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||||
|
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
|
||||||
|
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
|
||||||
|
// matches built-in interface blocks by their whole shape, and the two obvious spellings
|
||||||
|
// are both wrong:
|
||||||
|
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
|
||||||
|
// zeroes (degenerate triangles, nothing rasterized), and
|
||||||
|
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
|
||||||
|
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
|
||||||
|
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
|
||||||
|
// vertex+evaluation program through this same compiler and fails if the two shapes ever
|
||||||
|
// stop agreeing, rather than letting the mismatch show up as a black frame.
|
||||||
|
//
|
||||||
|
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||||
|
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||||
|
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||||
|
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||||
|
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||||
|
source += "in gl_PerVertex {\n"
|
||||||
|
" vec4 gl_Position;\n"
|
||||||
|
" float gl_PointSize;\n"
|
||||||
|
" float gl_ClipDistance[1];\n"
|
||||||
|
"} gl_in[gl_MaxPatchVertices];\n";
|
||||||
|
source += "out gl_PerVertex {\n"
|
||||||
|
" vec4 gl_Position;\n"
|
||||||
|
" float gl_PointSize;\n"
|
||||||
|
" float gl_ClipDistance[1];\n"
|
||||||
|
"} gl_out[];\n";
|
||||||
|
source += "void main() {\n";
|
||||||
|
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||||
|
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||||
|
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||||
|
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||||
|
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||||
|
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||||
|
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||||
|
source += "}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
|
||||||
|
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
|
||||||
|
// generator that cannot compile from re-running glslang on every draw.
|
||||||
|
const auto cached = m_passthroughTessControlStages.find(patchVertices);
|
||||||
|
if (cached != m_passthroughTessControlStages.end()) {
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||||
|
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||||
|
stage.module = VK_NULL_HANDLE;
|
||||||
|
stage.pName = "main";
|
||||||
|
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
const String source = BuildPassthroughTessControlSource(patchVertices);
|
||||||
|
// Same compile configuration as every other stage of every other program: this runs on
|
||||||
|
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
|
||||||
|
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
|
||||||
|
const SharedPtr<const CompileEnv>& env = GetCurrentCompileEnv();
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER,
|
||||||
|
.sourceStr = source,
|
||||||
|
.flags = 0,
|
||||||
|
.env = env.get()};
|
||||||
|
auto compiled = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
if (!compiled) {
|
||||||
|
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
|
||||||
|
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
|
||||||
|
patchVertices, compiled.error().log.c_str());
|
||||||
|
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{};
|
||||||
|
programAttrib.shaders.push_back(compiled.value());
|
||||||
|
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!linked) {
|
||||||
|
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
|
||||||
|
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
|
||||||
|
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()};
|
||||||
|
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!binary || binary.value().empty() || binary.value().front().empty()) {
|
||||||
|
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
|
||||||
|
"for patchVertices=%u", patchVertices);
|
||||||
|
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<Uint>& spirv = binary.value().front();
|
||||||
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
|
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||||
|
#else
|
||||||
|
if (m_enableSpirvValidation) {
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||||
|
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||||
|
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||||
|
smci.pCode = spirv.data();
|
||||||
|
VkShaderModule module = VK_NULL_HANDLE;
|
||||||
|
const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module);
|
||||||
|
if (result != VK_SUCCESS) {
|
||||||
|
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
|
||||||
|
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
|
||||||
|
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.module = module;
|
||||||
|
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
|
||||||
|
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
|
||||||
|
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
||||||
|
const Vector<ShaderStage>& stages,
|
||||||
|
const Vector<Vector<Uint>>& spirv,
|
||||||
|
VkProgramObject& entry) const {
|
||||||
|
entry.needsPassthroughTessControl = false;
|
||||||
|
entry.passthroughTessControlEmulatable = false;
|
||||||
|
|
||||||
|
Bool hasTessEval = false;
|
||||||
|
Bool hasTessControl = false;
|
||||||
|
SizeT tessEvalModuleIndex = 0;
|
||||||
|
for (SizeT i = 0; i < stages.size(); ++i) {
|
||||||
|
const ShaderStage stage = stages[i];
|
||||||
|
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
||||||
|
if (stage == ShaderStage::TessEval) {
|
||||||
|
hasTessEval = true;
|
||||||
|
tessEvalModuleIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasTessEval || hasTessControl) return;
|
||||||
|
|
||||||
|
entry.needsPassthroughTessControl = true;
|
||||||
|
|
||||||
|
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
|
||||||
|
const auto& module = spirv[tessEvalModuleIndex];
|
||||||
|
|
||||||
|
SpvReflectShaderModule reflectModule{};
|
||||||
|
const SpvReflectResult createResult =
|
||||||
|
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||||
|
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||||
|
MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the "
|
||||||
|
"evaluation stage's inputs are unknown, so the pass-through is not offered",
|
||||||
|
static_cast<Int>(createResult));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t inputCount = 0;
|
||||||
|
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
||||||
|
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
|
||||||
|
if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) {
|
||||||
|
reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data());
|
||||||
|
}
|
||||||
|
if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||||
|
spvReflectDestroyShaderModule(&reflectModule);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The question is only ever "does this stage read anything a control stage would have to
|
||||||
|
// forward", and the answer is: does it have a LOCATION. A located input is a user-defined
|
||||||
|
// varying (or a per-patch input), which the vertex stage writes today and would stop
|
||||||
|
// reaching once a control stage sits in between - the pass-through carries gl_Position and
|
||||||
|
// nothing else, so such a program is declined instead of being handed undefined values.
|
||||||
|
// Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||||
|
// gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation
|
||||||
|
// stage by the tessellator itself.
|
||||||
|
//
|
||||||
|
// This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an
|
||||||
|
// array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the
|
||||||
|
// block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a
|
||||||
|
// member walk reads "Position, Position, Position" for a {Position, PointSize,
|
||||||
|
// ClipDistance} block and would accept anything on the strength of parse garbage. The
|
||||||
|
// location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads
|
||||||
|
// straight through.
|
||||||
|
constexpr Uint32 kNoLocation = 0xFFFFFFFFu;
|
||||||
|
Bool emulatable = true;
|
||||||
|
for (auto* input : inputs) {
|
||||||
|
if (input == nullptr) continue;
|
||||||
|
if (input->location == kNoLocation) continue;
|
||||||
|
MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the "
|
||||||
|
"user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so "
|
||||||
|
"this program's draws are declined rather than fed an undefined varying",
|
||||||
|
input->name != nullptr ? input->name : "<null>", input->location);
|
||||||
|
emulatable = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvReflectDestroyShaderModule(&reflectModule);
|
||||||
|
entry.passthroughTessControlEmulatable = emulatable;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
CombinedImageSampler,
|
CombinedImageSampler,
|
||||||
UniformTexelBuffer,
|
UniformTexelBuffer,
|
||||||
StorageBuffer,
|
StorageBuffer,
|
||||||
StorageImage
|
StorageImage,
|
||||||
|
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
|
||||||
|
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
|
||||||
|
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
|
||||||
|
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
|
||||||
|
// DescriptorKeyHash mixes the enumerator's value.
|
||||||
|
StorageTexelBuffer
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class CompileOptionBit : Uint {
|
enum class CompileOptionBit : Uint {
|
||||||
@@ -70,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
|
|
||||||
|
struct UpdateAfterBindLimits {
|
||||||
|
Bool enabled = false;
|
||||||
|
Uint32 maxPerStageSamplers = 0;
|
||||||
|
Uint32 maxPerStageUniformBuffers = 0;
|
||||||
|
Uint32 maxPerStageStorageBuffers = 0;
|
||||||
|
Uint32 maxPerStageSampledImages = 0;
|
||||||
|
Uint32 maxPerStageStorageImages = 0;
|
||||||
|
Uint32 maxPerStageResources = 0;
|
||||||
|
Uint32 maxSetSamplers = 0;
|
||||||
|
Uint32 maxSetUniformBuffers = 0;
|
||||||
|
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||||
|
Uint32 maxSetStorageBuffers = 0;
|
||||||
|
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||||
|
Uint32 maxSetSampledImages = 0;
|
||||||
|
Uint32 maxSetStorageImages = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct VkProgramObject {
|
struct VkProgramObject {
|
||||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||||
|
|
||||||
@@ -82,6 +105,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
// Layout data (previously in separate VkProgramLayout)
|
// Layout data (previously in separate VkProgramLayout)
|
||||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
|
// True only when this layout passed every descriptor-indexing feature and
|
||||||
|
// update-after-bind limit gate at reflection time. It controls both the
|
||||||
|
// layout/binding flags and the pool class used by UniformManager.
|
||||||
|
Bool usesUpdateAfterBind = false;
|
||||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||||
Vector<DescriptorBindingKind> bindingKinds;
|
Vector<DescriptorBindingKind> bindingKinds;
|
||||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||||
@@ -103,6 +130,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Int> samplerUniformLocationByBinding;
|
Vector<Int> samplerUniformLocationByBinding;
|
||||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||||
|
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
|
||||||
|
// the other, never both, and both need exactly the same thing - the format the
|
||||||
|
// shader declared, so the per-draw resolve can tell a typed declaration from a
|
||||||
|
// formatless one. Kept as one pair rather than two so the move operations below
|
||||||
|
// cannot drift out of sync with a field that only one kind populates.
|
||||||
Vector<VkFormat> storageImageFormatByBinding;
|
Vector<VkFormat> storageImageFormatByBinding;
|
||||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||||
Vector<String> storageBlockNameByBinding;
|
Vector<String> storageBlockNameByBinding;
|
||||||
@@ -140,6 +172,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||||
// declared, so both variants answer the same and the draw path can ask either.
|
// declared, so both variants answer the same and the draw path can ask either.
|
||||||
Bool readsBaseVertexBuiltin = false;
|
Bool readsBaseVertexBuiltin = false;
|
||||||
|
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||||
|
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||||
|
// draws push the whole viewport/scissor array; every other program keeps the
|
||||||
|
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||||
|
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||||
|
Bool writesViewportIndexBuiltin = false;
|
||||||
|
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||||
|
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||||
|
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||||
|
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
|
||||||
|
// tessellation stages are present or neither
|
||||||
|
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||||
|
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||||
|
Bool needsPassthroughTessControl = false;
|
||||||
|
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||||
|
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||||
|
// built-ins. A user-defined varying would arrive at the evaluation stage
|
||||||
|
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
|
||||||
|
// silently wrong pixels rather than a crash - so those programs are declined
|
||||||
|
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||||
|
// skipped). See ReflectPassthroughTessControlNeed.
|
||||||
|
Bool passthroughTessControlEmulatable = false;
|
||||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||||
@@ -163,6 +217,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// a pipeline failure would be reported against the wrong SPIR-V.
|
// a pipeline failure would be reported against the wrong SPIR-V.
|
||||||
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
||||||
descriptorSetLayout = other.descriptorSetLayout;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
|
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
activeBindings = std::move(other.activeBindings);
|
activeBindings = std::move(other.activeBindings);
|
||||||
@@ -191,9 +246,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
|
other.usesUpdateAfterBind = false;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
other.hasStorageImages = false;
|
other.hasStorageImages = false;
|
||||||
other.declinedDescriptors = false;
|
other.declinedDescriptors = false;
|
||||||
@@ -205,6 +264,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.readsBaseVertexBuiltin = false;
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.writesViewportIndexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
@@ -217,6 +279,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
modules = std::move(other.modules);
|
modules = std::move(other.modules);
|
||||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||||
descriptorSetLayout = other.descriptorSetLayout;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
|
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
activeBindings = std::move(other.activeBindings);
|
activeBindings = std::move(other.activeBindings);
|
||||||
@@ -245,9 +308,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
|
other.usesUpdateAfterBind = false;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
other.hasStorageImages = false;
|
other.hasStorageImages = false;
|
||||||
other.declinedDescriptors = false;
|
other.declinedDescriptors = false;
|
||||||
@@ -259,6 +326,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.readsBaseVertexBuiltin = false;
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.writesViewportIndexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -302,15 +372,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
||||||
Bool shaderDrawParametersEnabled = false,
|
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
||||||
Bool unformattedFloatStorageImagesEnabled = false)
|
// subgroup properties) so lowering can never disagree with the advertised
|
||||||
|
// capabilities. Native subgroup operations always execute natively; the two
|
||||||
|
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||||
|
// on opted-in devices with no subgroup support at all.
|
||||||
|
struct SubgroupLoweringPolicy {
|
||||||
|
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||||
|
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||||
|
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
||||||
|
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||||
|
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
||||||
|
Uint32 nativeSubgroupSize = 0;
|
||||||
|
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
||||||
|
// workgroup needs more subgroups than this cannot request the flag.
|
||||||
|
Uint32 maxComputeWorkgroupSubgroups = 0;
|
||||||
|
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
||||||
|
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
||||||
|
Uint32 maxComputeSharedMemoryBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||||
|
Bool shaderDrawParametersEnabled,
|
||||||
|
Bool unformattedFloatStorageImagesEnabled,
|
||||||
|
Bool enableSpirvValidation,
|
||||||
|
UpdateAfterBindLimits updateAfterBindLimits,
|
||||||
|
SubgroupLoweringPolicy subgroupPolicy)
|
||||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||||
|
m_enableSpirvValidation(enableSpirvValidation),
|
||||||
|
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||||
|
m_subgroupPolicy(subgroupPolicy) {
|
||||||
VkProgramObject::s_device = device;
|
VkProgramObject::s_device = device;
|
||||||
}
|
}
|
||||||
~ProgramFactory() = default;
|
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||||
|
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||||
|
// nothing that owns the device.
|
||||||
|
~ProgramFactory();
|
||||||
ProgramFactory(const ProgramFactory&) = delete;
|
ProgramFactory(const ProgramFactory&) = delete;
|
||||||
|
|
||||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||||
@@ -362,6 +462,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Shared by the two above: does any entry point list an input variable decorated with
|
// Shared by the two above: does any entry point list an input variable decorated with
|
||||||
// this builtin?
|
// this builtin?
|
||||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||||
|
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||||
|
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||||
|
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||||
|
// a pre-rasterization stage declares it in.
|
||||||
|
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||||
|
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||||
|
|
||||||
|
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||||
|
// program that has an evaluation stage and no control stage, for an input patch of
|
||||||
|
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
|
||||||
|
// the cache below is a rehashing map, so a pointer into it would not survive the next
|
||||||
|
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
|
||||||
|
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||||
|
// pipeline rather than handing the driver a half-tessellated one.
|
||||||
|
//
|
||||||
|
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||||
|
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||||
|
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||||
|
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||||
|
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||||
|
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||||
|
// happens for the rare program that has no control stage at all.
|
||||||
|
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||||
|
|
||||||
|
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||||
|
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||||
|
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
@@ -372,14 +499,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
||||||
|
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
||||||
|
// keeps the program's live attach list, which is a longer and differently-indexed list
|
||||||
|
// the moment a glAttachShader lands after the link, from being passed here by mistake.
|
||||||
|
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||||
|
const Vector<Vector<Uint>>& spirv,
|
||||||
|
VkProgramObject& entry) const;
|
||||||
|
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
|
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||||
|
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||||
|
// records what the evaluation stage's input interface is made of.
|
||||||
|
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
|
||||||
|
const Vector<Vector<Uint>>& spirv,
|
||||||
|
VkProgramObject& entry) const;
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
Uint32 m_maxBindings = 0;
|
Uint32 m_maxBindings = 0;
|
||||||
@@ -391,6 +531,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True only when the logical device enabled both
|
// True only when the logical device enabled both
|
||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
|
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||||
|
// originate from a ProgramLinkTask.
|
||||||
|
Bool m_enableSpirvValidation = false;
|
||||||
|
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||||
|
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||||
|
// own counts would exceed the update-after-bind budget.
|
||||||
|
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||||
|
SubgroupLoweringPolicy m_subgroupPolicy{};
|
||||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||||
Uint32 m_defaultFramebufferHeight = 0;
|
Uint32 m_defaultFramebufferHeight = 0;
|
||||||
@@ -400,6 +548,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||||
Uint64 m_cacheStructureEpoch = 1;
|
Uint64 m_cacheStructureEpoch = 1;
|
||||||
IEvictionObserver* m_evictionObserver = nullptr;
|
IEvictionObserver* m_evictionObserver = nullptr;
|
||||||
|
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||||
|
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||||
|
// ever built from one keeps referencing its module. A failed build is cached as
|
||||||
|
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||||
|
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||||
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||||
|
|||||||
@@ -156,13 +156,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frame.descriptorPools.clear();
|
frame.descriptorPools.clear();
|
||||||
|
|
||||||
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
||||||
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
|
if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) {
|
||||||
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||||
frameIndex);
|
frameIndex);
|
||||||
Shutdown();
|
Shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0});
|
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false});
|
||||||
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
|
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
|
||||||
m_setsPerFrame);
|
m_setsPerFrame);
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||||
// and view lookups - would recompute the identical descriptor.
|
// and view lookups - would recompute the identical descriptor.
|
||||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
||||||
m_samplerResolveMemo[binding].infoValid) {
|
m_samplerResolveMemo[binding].infoValid &&
|
||||||
|
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
||||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -345,13 +346,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||||
texture = fallbackHolder.get();
|
texture = fallbackHolder.get();
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||||
"location=%d unit=%d target=%d",
|
"location=%d unit=%d target=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||||
static_cast<Int>(preferredTarget));
|
static_cast<Int>(preferredTarget));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MGLOG_W(
|
MGLOG_W_ONCE(
|
||||||
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||||
static_cast<Int>(preferredTarget));
|
static_cast<Int>(preferredTarget));
|
||||||
@@ -360,7 +361,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::SamplerObject* samplerToUse =
|
const MG_State::GLState::SamplerObject* samplerToUse =
|
||||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||||
if (samplerToUse == nullptr) {
|
if (samplerToUse == nullptr) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
|
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
|
||||||
unit);
|
unit);
|
||||||
@@ -368,7 +369,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||||
if (resource == nullptr) {
|
if (resource == nullptr) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
|
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
static_cast<Int>(texture->GetTarget()), location, unit);
|
static_cast<Int>(texture->GetTarget()), location, unit);
|
||||||
@@ -380,7 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Int attachmentLevel = 0;
|
Int attachmentLevel = 0;
|
||||||
if (drawFbo &&
|
if (drawFbo &&
|
||||||
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
|
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
|
||||||
MGLOG_W("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
||||||
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
|
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
|
||||||
"trackedLayout=%d)",
|
"trackedLayout=%d)",
|
||||||
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
|
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
|
||||||
@@ -390,7 +391,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
|
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
|
||||||
if (!readyForSampling) {
|
if (!readyForSampling) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
||||||
texture->GetExternalIndex(), binding);
|
texture->GetExternalIndex(), binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -432,7 +433,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||||
"textureId=%d imageFormat=%d numericDomain=%d",
|
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||||
@@ -445,7 +446,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
? resource->sampledView
|
? resource->sampledView
|
||||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||||
if (sampledImageView == VK_NULL_HANDLE) {
|
if (sampledImageView == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||||
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
||||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (binding < m_samplerResolveMemo.size()) {
|
if (binding < m_samplerResolveMemo.size()) {
|
||||||
if (descriptorMemoUsable) {
|
if (descriptorMemoUsable) {
|
||||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||||
|
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||||
m_samplerResolveMemo[binding].infoValid = true;
|
m_samplerResolveMemo[binding].infoValid = true;
|
||||||
} else {
|
} else {
|
||||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||||
@@ -540,11 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
outImageInfo = {
|
outImageInfo = {
|
||||||
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
||||||
*samplerBindingOverride.texture),
|
*samplerBindingOverride.texture,
|
||||||
|
samplerBindingOverride.forceNearestFiltering,
|
||||||
|
resource->sampledLevelCount),
|
||||||
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
||||||
samplerBindingOverride.imageView :
|
samplerBindingOverride.imageView :
|
||||||
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
||||||
.imageLayout = resource->layout,
|
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
||||||
|
samplerBindingOverride.imageLayout : resource->layout,
|
||||||
};
|
};
|
||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -671,14 +676,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
||||||
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||||
programObj.samplerNameByBinding[binding].c_str());
|
programObj.samplerNameByBinding[binding].c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
|
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
|
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
|
||||||
@@ -688,14 +693,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||||
if (bufferObject == nullptr) {
|
if (bufferObject == nullptr) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||||
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -703,7 +708,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto internalFormat = textureBuffer->GetFormat();
|
const auto internalFormat = textureBuffer->GetFormat();
|
||||||
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
||||||
static_cast<Int>(internalFormat));
|
static_cast<Int>(internalFormat));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -719,7 +724,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewRange = (viewRange / texelSize) * texelSize;
|
viewRange = (viewRange / texelSize) * texelSize;
|
||||||
}
|
}
|
||||||
if (viewRange == 0) {
|
if (viewRange == 0) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,7 +738,151 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||||
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||||
|
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frames[frameIndex].texelBufferViews.push_back(bufferView);
|
||||||
|
outBufferView = bufferView;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather
|
||||||
|
// than a VkImageView, so it is half ResolveStorageImageDescriptor (the resource comes from an
|
||||||
|
// IMAGE unit, i.e. from glBindImageTexture, not from a texture unit) and half
|
||||||
|
// ResolveTexelBufferDescriptor (the descriptor is a buffer view over the GL buffer the
|
||||||
|
// texture is attached to).
|
||||||
|
//
|
||||||
|
// Before this existed the descriptor kind reflected as SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_-
|
||||||
|
// TEXEL_BUFFER and fell into ReflectDescriptorTypeToBindingKind's `default:`, whose only
|
||||||
|
// complaint is an assert that compiles out above DEBUG - so a release build declared no
|
||||||
|
// binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline
|
||||||
|
// creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the
|
||||||
|
// case that carries it.
|
||||||
|
Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Uint32 binding, Uint32 frameIndex,
|
||||||
|
VkBufferView& outBufferView) {
|
||||||
|
outBufferView = VK_NULL_HANDLE;
|
||||||
|
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||||
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
|
||||||
|
"ResolveStorageTexelBufferDescriptor: frame index out of range");
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||||
|
"ResolveStorageTexelBufferDescriptor: binding %u out of range", binding);
|
||||||
|
|
||||||
|
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||||
|
if (location < 0) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
|
||||||
|
programObj.samplerNameByBinding[binding].c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||||
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
|
||||||
|
binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||||
|
const auto& texture = imageBinding.Texture;
|
||||||
|
if (texture == nullptr) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||||
|
binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||||
|
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
||||||
|
"unit %d, got textureId=%u target=%d storage=%d",
|
||||||
|
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
|
||||||
|
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
|
||||||
|
static_cast<Int>(texture->GetStorageType()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||||
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||||
|
if (bufferObject == nullptr) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||||
|
imageUnit);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the sampled texel buffer, the shader MAY write this one, and those writes land
|
||||||
|
// in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and
|
||||||
|
// GetBufferSubData read. Same two calls, and for the same reason, as the storage-block
|
||||||
|
// path above - but only the residency is unconditional. Marking a GL_READ_ONLY binding
|
||||||
|
// GPU-written would make the next map or readback wait for a dispatch that could not have
|
||||||
|
// changed a byte of it.
|
||||||
|
bufferObject->EnsureGpuResidentStorage();
|
||||||
|
if (imageBinding.Access != GL_READ_ONLY) {
|
||||||
|
bufferObject->MarkGpuWritten();
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferSlice slice{};
|
||||||
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
|
||||||
|
!slice.IsValid()) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||||
|
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The format the SHADER declared wins over the one glBindImageTexture named, on the same
|
||||||
|
// policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as
|
||||||
|
// r32ui whatever the texture's own attachment format says. Falling back, in order:
|
||||||
|
// reflected format, then the bind format, then the texture's attached format.
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||||
|
"ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding);
|
||||||
|
const auto internalFormat = textureBuffer->GetFormat();
|
||||||
|
const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||||
|
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||||
|
VkFormat vkFormat = reflectedFormat;
|
||||||
|
if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) {
|
||||||
|
vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
||||||
|
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||||
|
}
|
||||||
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
vkFormat = resourceFormat;
|
||||||
|
}
|
||||||
|
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
|
||||||
|
static_cast<Int>(internalFormat), imageBinding.Format);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sized from the TEXTURE's attached format even though the view may carry a different
|
||||||
|
// one. That is not a shortcut: GL requires the shader's format qualifier, the format
|
||||||
|
// passed to glBindImageTexture and the texture's own internal format to belong to the
|
||||||
|
// same format CLASS (GL 4.6 core, table 8.27), and every member of a class has the same
|
||||||
|
// texel size. So the three can disagree on interpretation and never on bytes - which is
|
||||||
|
// what the range below has to be a whole multiple of.
|
||||||
|
const VkDeviceSize texelSize =
|
||||||
|
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
||||||
|
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
||||||
|
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
||||||
|
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
||||||
|
if (texelSize > 0) {
|
||||||
|
viewRange = (viewRange / texelSize) * texelSize;
|
||||||
|
}
|
||||||
|
if (viewRange == 0) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
|
||||||
|
texture->GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkBufferViewCreateInfo viewInfo{};
|
||||||
|
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
||||||
|
viewInfo.buffer = slice.buffer;
|
||||||
|
viewInfo.format = vkFormat;
|
||||||
|
viewInfo.offset = slice.offset + rangeOffset;
|
||||||
|
viewInfo.range = viewRange;
|
||||||
|
|
||||||
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
|
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||||
|
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||||
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -770,7 +919,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
|
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
|
||||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||||
if (bufferObject == nullptr) {
|
if (bufferObject == nullptr) {
|
||||||
MGLOG_E("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||||
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -785,7 +934,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||||
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||||
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
|
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -799,7 +948,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
rangeEnd = bufferSize;
|
rangeEnd = bufferSize;
|
||||||
}
|
}
|
||||||
if (rangeEnd <= rangeStart) {
|
if (rangeEnd <= rangeStart) {
|
||||||
MGLOG_E("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
||||||
programObj.storageBlockNameByBinding[binding].c_str());
|
programObj.storageBlockNameByBinding[binding].c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -823,7 +972,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||||
if (baseLocation < 0) {
|
if (baseLocation < 0) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
|
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
|
||||||
@@ -835,26 +984,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// uniform.
|
// uniform.
|
||||||
const Int location = baseLocation + static_cast<Int>(element);
|
const Int location = baseLocation + static_cast<Int>(element);
|
||||||
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
|
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
||||||
binding, element);
|
binding, element);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
||||||
imageUnit, binding);
|
imageUnit, binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||||
if (imageBinding.Texture == nullptr) {
|
if (imageBinding.Texture == nullptr) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
||||||
imageBinding.Texture->GetExternalIndex(), imageUnit);
|
imageBinding.Texture->GetExternalIndex(), imageUnit);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -874,7 +1023,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||||
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
||||||
useBindingFormat ? "true" : "false");
|
useBindingFormat ? "true" : "false");
|
||||||
@@ -883,7 +1032,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||||
if (view == VK_NULL_HANDLE) {
|
if (view == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||||
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
||||||
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
||||||
@@ -904,7 +1053,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||||
// process over an unbound sampler is never the right answer.
|
// process over an unbound sampler is never the right answer.
|
||||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||||
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||||
static_cast<Int>(target));
|
static_cast<Int>(target));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -1080,13 +1229,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
||||||
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||||
if (baseLocation < 0) {
|
if (baseLocation < 0) {
|
||||||
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
|
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
|
||||||
@@ -1098,20 +1247,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
|
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
|
||||||
if (location < 0) {
|
if (location < 0) {
|
||||||
MGLOG_E("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
||||||
binding, element);
|
binding, element);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||||
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
||||||
imageUnit, binding, element);
|
imageUnit, binding, element);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||||
imageUnit, binding, element);
|
imageUnit, binding, element);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1123,6 +1272,87 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||||
|
GLint imageLevel, GLenum imageAccess) {
|
||||||
|
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::CollectSamplerImageFeedback(
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
||||||
|
outBindings.clear();
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||||
|
"CollectSamplerImageFeedback: GL context is null");
|
||||||
|
if (programObj.declinedDescriptors) return true;
|
||||||
|
|
||||||
|
for (const Uint32 samplerBinding : programObj.activeBindings) {
|
||||||
|
if (samplerBinding >= m_maxBindings ||
|
||||||
|
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
|
||||||
|
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
|
||||||
|
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
|
||||||
|
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
|
||||||
|
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
||||||
|
sampledTexture, sampledSampler) ||
|
||||||
|
sampledTexture == nullptr || sampledSampler == nullptr ||
|
||||||
|
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
|
||||||
|
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
||||||
|
// alias the image-unit binding of the original texture.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
||||||
|
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
|
||||||
|
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
|
||||||
|
const TextureTarget sampledTarget = sampledTexture->GetTarget();
|
||||||
|
if (sampledTarget == TextureTarget::Texture2DMultisample ||
|
||||||
|
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& levelRange = sampledTexture->GetLevelRange();
|
||||||
|
Bool aliasesWritableImage = false;
|
||||||
|
for (const Uint32 imageBinding : programObj.activeBindings) {
|
||||||
|
if (imageBinding >= m_maxBindings ||
|
||||||
|
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
|
||||||
|
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
|
||||||
|
if (baseLocation < 0) return false;
|
||||||
|
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
|
||||||
|
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
|
||||||
|
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
|
||||||
|
if (location < 0) return false;
|
||||||
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||||
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||||
|
// A sampler view exposes all layers of its target; equal texture plus an
|
||||||
|
// overlapping mip therefore aliases the writable image subresource.
|
||||||
|
if (image.Texture.get() == sampledTexture &&
|
||||||
|
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
|
||||||
|
image.Level, image.Access)) {
|
||||||
|
aliasesWritableImage = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aliasesWritableImage) break;
|
||||||
|
}
|
||||||
|
if (aliasesWritableImage) {
|
||||||
|
outBindings.push_back({.samplerBinding = samplerBinding,
|
||||||
|
.samplerElement = samplerElement,
|
||||||
|
.texture = sampledTexture,
|
||||||
|
.sampler = sampledSampler,
|
||||||
|
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 arrayElement, UboBindResult& out) const {
|
Uint32 arrayElement, UboBindResult& out) const {
|
||||||
@@ -1244,7 +1474,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const {
|
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const {
|
||||||
outPool = VK_NULL_HANDLE;
|
outPool = VK_NULL_HANDLE;
|
||||||
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
|
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1262,12 +1492,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Uint64 descriptorCount64 =
|
const Uint64 descriptorCount64 =
|
||||||
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
||||||
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
||||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
|
const Uint32 descriptorCount = static_cast<Uint32>(descriptorCount64);
|
||||||
VkDescriptorPoolSize poolSizes[5]{};
|
VkDescriptorPoolSize poolSizes[6]{};
|
||||||
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||||
poolSizes[0].descriptorCount = descriptorCount;
|
poolSizes[0].descriptorCount = descriptorCount;
|
||||||
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
@@ -1278,6 +1508,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
poolSizes[3].descriptorCount = descriptorCount;
|
poolSizes[3].descriptorCount = descriptorCount;
|
||||||
poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
poolSizes[4].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||||
poolSizes[4].descriptorCount = descriptorCount;
|
poolSizes[4].descriptorCount = descriptorCount;
|
||||||
|
poolSizes[5].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||||
|
poolSizes[5].descriptorCount = descriptorCount;
|
||||||
|
|
||||||
VkDescriptorPoolCreateInfo poolInfo{};
|
VkDescriptorPoolCreateInfo poolInfo{};
|
||||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||||
@@ -1285,38 +1517,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||||
// cache grows - never on the per-draw reuse path.
|
// cache grows - never on the per-draw reuse path.
|
||||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT |
|
||||||
|
(updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0);
|
||||||
poolInfo.maxSets = maxSets;
|
poolInfo.maxSets = maxSets;
|
||||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||||
poolInfo.pPoolSizes = poolSizes;
|
poolInfo.pPoolSizes = poolSizes;
|
||||||
|
|
||||||
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
|
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
||||||
result);
|
result);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex) {
|
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
||||||
if (frame.descriptorPools.empty()) {
|
if (frame.descriptorPools.empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& currentBucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
|
const auto matchingBucket = std::find_if(
|
||||||
const Uint32 currentMaxSets = std::max<Uint32>(1, currentBucket.maxSets);
|
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||||
|
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
|
||||||
|
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
|
||||||
|
? std::max<Uint32>(1, matchingBucket->maxSets)
|
||||||
|
: m_setsPerFrame;
|
||||||
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
||||||
: currentMaxSets;
|
: currentMaxSets;
|
||||||
|
|
||||||
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
||||||
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
|
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
||||||
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||||
currentMaxSets, grownMaxSets);
|
currentMaxSets, grownMaxSets);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0});
|
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
||||||
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
||||||
MGLOG_D(
|
MGLOG_D(
|
||||||
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
||||||
@@ -1326,14 +1563,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
|
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
|
||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
const Bool updateAfterBind = programObj.usesUpdateAfterBind;
|
||||||
frame.activeDescriptorPoolIndex = 0;
|
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() ||
|
||||||
}
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind ||
|
||||||
if (frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
||||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
||||||
const auto availableBucket = std::find_if(
|
const auto availableBucket = std::find_if(
|
||||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||||
[](const DescriptorPoolBucket& candidate) { return candidate.allocatedSets < candidate.maxSets; });
|
[updateAfterBind](const DescriptorPoolBucket& candidate) {
|
||||||
|
return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets;
|
||||||
|
});
|
||||||
if (availableBucket == frame.descriptorPools.end()) {
|
if (availableBucket == frame.descriptorPools.end()) {
|
||||||
outDescriptorSet = VK_NULL_HANDLE;
|
outDescriptorSet = VK_NULL_HANDLE;
|
||||||
return VK_ERROR_OUT_OF_POOL_MEMORY;
|
return VK_ERROR_OUT_OF_POOL_MEMORY;
|
||||||
@@ -1369,8 +1608,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
} else {
|
} else {
|
||||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||||
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
|
if (!GrowFrameDescriptorPool(frame, frameIndex, programObj.usesUpdateAfterBind)) {
|
||||||
MGLOG_E("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||||
return allocResult;
|
return allocResult;
|
||||||
}
|
}
|
||||||
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||||
@@ -1487,7 +1726,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint,
|
VkPipelineBindPoint bindPoint,
|
||||||
const SamplerBindingOverride* samplerBindingOverride,
|
const SamplerBindingOverride* samplerBindingOverride,
|
||||||
Bool samplerDescriptorsUnchangedHint) {
|
Bool samplerDescriptorsUnchangedHint,
|
||||||
|
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
|
||||||
// This program has a descriptor MobileGL could not resolve (see
|
// This program has a descriptor MobileGL could not resolve (see
|
||||||
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
|
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
|
||||||
// binding is still declared in the layout, so the pipeline is consistent with the shader
|
// binding is still declared in the layout, so the pipeline is consistent with the shader
|
||||||
@@ -1501,7 +1741,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
if (frame.descriptorPools.empty()) {
|
if (frame.descriptorPools.empty()) {
|
||||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
||||||
@@ -1514,7 +1754,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// sampler binding, and an unchanged (buffer, range) for the single
|
// sampler binding, and an unchanged (buffer, range) for the single
|
||||||
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
||||||
// the SAME set delivers without any descriptor write.
|
// the SAME set delivers without any descriptor write.
|
||||||
const Bool cacheable = (samplerBindingOverride == nullptr);
|
const Bool cacheable = samplerBindingOverride == nullptr &&
|
||||||
|
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
|
||||||
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
||||||
m_fastRebindMemo.frameIndex == frameIndex &&
|
m_fastRebindMemo.frameIndex == frameIndex &&
|
||||||
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
||||||
@@ -1578,6 +1819,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali),
|
// is reachable wherever m_maxBindings is small (it clamps to ~16 on Adreno and Mali),
|
||||||
// which is exactly where a 7-element CTS sampler array does not fit the slack.
|
// which is exactly where a 7-element CTS sampler array does not fit the slack.
|
||||||
imageInfos.reserve(m_maxBindings + arrayDescriptorExtra);
|
imageInfos.reserve(m_maxBindings + arrayDescriptorExtra);
|
||||||
|
// Exact, and safe only because it is: BOTH texel kinds (samplerBuffer and imageBuffer)
|
||||||
|
// refuse descriptor arrays at program creation, so each contributes at most one view and
|
||||||
|
// the total cannot exceed the binding count. The branches below take the address of
|
||||||
|
// back(), so making a texel kind array-capable without also giving this the surplus
|
||||||
|
// imageInfos gets would dangle every pTexelBufferView already recorded in `writes`.
|
||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||||
|
|
||||||
@@ -1633,7 +1879,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||||
bufferView == VK_NULL_HANDLE) {
|
bufferView == VK_NULL_HANDLE) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
|
||||||
binding);
|
binding);
|
||||||
return false;
|
return false;
|
||||||
@@ -1644,6 +1890,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||||
write.pTexelBufferView = &texelBufferViews.back();
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer) {
|
||||||
|
// Shares texelBufferViews with the sampled kind above, and may do so safely for
|
||||||
|
// the same reason: neither kind can be an array, so each contributes exactly one
|
||||||
|
// element and the reserve of m_maxBindings cannot be outrun - which is what keeps
|
||||||
|
// the &back() below from dangling when a later binding pushes.
|
||||||
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
|
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||||
|
bufferView == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
|
||||||
|
"has no valid descriptor",
|
||||||
|
binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
texelBufferViews.push_back(bufferView);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
|
||||||
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
|
writes.push_back(write);
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
||||||
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
||||||
// instance array occupies a single binding whose elements each come from their
|
// instance array occupies a single binding whose elements each come from their
|
||||||
@@ -1653,7 +1918,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
VkDescriptorBufferInfo bufferInfo{};
|
VkDescriptorBufferInfo bufferInfo{};
|
||||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
||||||
"element %u has no valid descriptor",
|
"element %u has no valid descriptor",
|
||||||
binding, element);
|
binding, element);
|
||||||
@@ -1680,7 +1945,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorImageInfo imageInfo{};
|
VkDescriptorImageInfo imageInfo{};
|
||||||
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
|
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
|
||||||
imageInfo)) {
|
imageInfo)) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
|
||||||
"element %u has no valid descriptor",
|
"element %u has no valid descriptor",
|
||||||
binding, element);
|
binding, element);
|
||||||
@@ -1714,22 +1979,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const SizeT firstImageInfoIndex = imageInfos.size();
|
const SizeT firstImageInfoIndex = imageInfos.size();
|
||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
VkDescriptorImageInfo imageInfo{};
|
VkDescriptorImageInfo imageInfo{};
|
||||||
Bool hasImage = false;
|
const SamplerBindingOverride* overrideForElement =
|
||||||
if (overrideThisBinding && element == 0) {
|
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
|
||||||
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
|
||||||
} else {
|
const auto overrideIt = std::find_if(
|
||||||
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
|
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
|
||||||
imageInfo, samplerDescriptorsUnchangedHint);
|
[binding, element](const SamplerBindingOverride& candidate) {
|
||||||
|
return candidate.binding == binding && candidate.element == element;
|
||||||
|
});
|
||||||
|
if (overrideIt != samplerBindingOverrides->end()) {
|
||||||
|
overrideForElement = &*overrideIt;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const Bool hasImage = overrideForElement != nullptr
|
||||||
|
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
|
||||||
|
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
|
||||||
|
element, imageInfo,
|
||||||
|
samplerDescriptorsUnchangedHint);
|
||||||
if (!hasImage) {
|
if (!hasImage) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||||
"has no valid texture descriptor",
|
"has no valid texture descriptor",
|
||||||
binding, element);
|
binding, element);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
|
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
|
||||||
MGLOG_E(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||||
"has null sampler or imageView",
|
"has null sampler or imageView",
|
||||||
binding, element);
|
binding, element);
|
||||||
@@ -1803,7 +2078,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
} else {
|
} else {
|
||||||
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
||||||
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
||||||
allocResult);
|
allocResult);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
public:
|
public:
|
||||||
struct SamplerBindingOverride {
|
struct SamplerBindingOverride {
|
||||||
Uint32 binding = 0;
|
Uint32 binding = 0;
|
||||||
|
Uint32 element = 0;
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
VkImageView imageView = VK_NULL_HANDLE;
|
VkImageView imageView = VK_NULL_HANDLE;
|
||||||
|
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
Bool forceNearestFiltering = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SamplerImageFeedbackBinding {
|
||||||
|
Uint32 samplerBinding = 0;
|
||||||
|
Uint32 samplerElement = 0;
|
||||||
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
|
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||||
@@ -79,6 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||||
|
Bool CollectSamplerImageFeedback(
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||||
|
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||||
|
GLint imageLevel, GLenum imageAccess);
|
||||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||||
// every input of every combined-image-sampler resolution is unchanged since the
|
// every input of every combined-image-sampler resolution is unchanged since the
|
||||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||||
@@ -91,7 +108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||||
Bool samplerDescriptorsUnchangedHint = false);
|
Bool samplerDescriptorsUnchangedHint = false,
|
||||||
|
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||||
|
|
||||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||||
@@ -114,6 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||||
Uint32 maxSets = 0;
|
Uint32 maxSets = 0;
|
||||||
Uint32 allocatedSets = 0;
|
Uint32 allocatedSets = 0;
|
||||||
|
Bool updateAfterBind = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A cached descriptor set together with the pool it was allocated from, so a
|
// A cached descriptor set together with the pool it was allocated from, so a
|
||||||
@@ -175,6 +194,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||||
|
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
|
||||||
|
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
|
||||||
|
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
|
||||||
|
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
|
||||||
|
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
|
||||||
|
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||||
// binding point, buffer and glBindBufferRange window.
|
// binding point, buffer and glBindBufferRange window.
|
||||||
@@ -215,8 +242,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||||
const Vector<Uint32>& dynamicOffsets);
|
const Vector<Uint32>& dynamicOffsets);
|
||||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||||
VkResult AllocateDescriptorSetsFromActivePool(
|
VkResult AllocateDescriptorSetsFromActivePool(
|
||||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||||
@@ -333,8 +360,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||||
|
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||||
|
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||||
struct SamplerResolveMemo {
|
struct SamplerResolveMemo {
|
||||||
|
Uint64 infoProgramLifetimeId = 0;
|
||||||
Uint64 samplerLifetimeId = 0;
|
Uint64 samplerLifetimeId = 0;
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
VkSampler sampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -107,10 +108,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VkFormat sourceVkFormat =
|
VkFormat sourceVkFormat =
|
||||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||||
|
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||||
|
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||||
|
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||||
|
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||||
|
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||||
|
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||||
|
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||||
|
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||||
|
// module, so a float32 stream would be fed to a Float64 input.
|
||||||
|
const Bool narrowFloat64Arrays =
|
||||||
|
MG_Backend::pActiveBackendObject == nullptr ||
|
||||||
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
||||||
|
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
||||||
|
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
||||||
|
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
||||||
|
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
||||||
|
// than dropped. Mirrors what DirectGLES does for the same state.
|
||||||
|
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
||||||
|
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
||||||
|
sourceVkFormat = narrowedFormat;
|
||||||
|
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||||
|
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
||||||
|
"float32 precision through format=%d (size=%d long=%s)",
|
||||||
|
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||||
"enabled but cannot be mapped to a VkFormat",
|
"enabled but cannot be mapped to a VkFormat",
|
||||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||||
unsupportedAttribMask |= (1u << location);
|
unsupportedAttribMask |= (1u << location);
|
||||||
@@ -118,14 +145,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkFormat vkFormat = sourceVkFormat;
|
VkFormat vkFormat = sourceVkFormat;
|
||||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
||||||
if (!SupportsVertexBufferFormat(vkFormat)) {
|
|
||||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||||
vkFormat = fallbackFormat;
|
vkFormat = fallbackFormat;
|
||||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||||
"(type=%s size=%d normalized=%s integer=%s)",
|
"(type=%s size=%d normalized=%s integer=%s)",
|
||||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||||
@@ -135,7 +161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (conversion == VertexStreamConversion::None) {
|
if (conversion == VertexStreamConversion::None) {
|
||||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||||
location, static_cast<Int>(sourceVkFormat),
|
location, static_cast<Int>(sourceVkFormat),
|
||||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||||
@@ -146,7 +172,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||||
if (attribByteSize == 0) {
|
if (attribByteSize == 0) {
|
||||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||||
"enabled but cannot be sized",
|
"enabled but cannot be sized",
|
||||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||||
unsupportedAttribMask |= (1u << location);
|
unsupportedAttribMask |= (1u << location);
|
||||||
@@ -175,7 +201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||||
// attribute into a tightly packed transient stream without changing its format.
|
// attribute into a tightly packed transient stream without changing its format.
|
||||||
conversion = VertexStreamConversion::Repack;
|
conversion = VertexStreamConversion::Repack;
|
||||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||||
location, attr.Offset, sourceStride, requiredAlignment);
|
location, attr.Offset, sourceStride, requiredAlignment);
|
||||||
}
|
}
|
||||||
@@ -188,7 +214,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (sourceStride != 0) {
|
if (sourceStride != 0) {
|
||||||
if (conversion == VertexStreamConversion::Repack) {
|
if (conversion == VertexStreamConversion::Repack) {
|
||||||
stride = static_cast<Uint32>(attribByteSize);
|
stride = static_cast<Uint32>(attribByteSize);
|
||||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
||||||
|
conversion == VertexStreamConversion::Float64ToFloat32) {
|
||||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,8 +314,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||||
it = m_cache.erase(it);
|
it = m_cache.erase(it);
|
||||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||||
// address may be reused by a future insert.
|
// address may be reused by a future insert. Advance through the
|
||||||
++m_evictionEpoch;
|
// process-wide source so the value stays unique across factory
|
||||||
|
// instances (see the member comment).
|
||||||
|
m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
@@ -328,6 +357,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||||
// so they always agree without extra plumbing.
|
// so they always agree without extra plumbing.
|
||||||
|
//
|
||||||
|
// ... as long as the shader half still runs. It does not when the backend has declared
|
||||||
|
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
|
||||||
|
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
|
||||||
|
// and a UINT-formatted attribute would be fed to a float input - garbage with no
|
||||||
|
// diagnostic anywhere. Declining here hands the attribute to the caller's
|
||||||
|
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
|
||||||
|
// demoted `vec` input - the same thing DirectGLES does for the same state. The
|
||||||
|
// frontend RECORDS the format either way, so this gate is the only thing standing
|
||||||
|
// between a legal glVertexAttribLFormat and a mismatched pipeline.
|
||||||
|
if (MG_Backend::pActiveBackendObject == nullptr ||
|
||||||
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 1: return VK_FORMAT_R32G32_UINT;
|
case 1: return VK_FORMAT_R32G32_UINT;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
None = 0,
|
None = 0,
|
||||||
Repack,
|
Repack,
|
||||||
ScaledIntegerToFloat32,
|
ScaledIntegerToFloat32,
|
||||||
|
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
|
||||||
|
// of the fp64 demotion the shader side already does unconditionally.
|
||||||
|
Float64ToFloat32,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
@@ -125,7 +128,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// construction); a memo is honored only while its recorded epoch
|
// construction); a memo is honored only while its recorded epoch
|
||||||
// matches, so an evicted entry can never be dereferenced through a
|
// matches, so an evicted entry can never be dereferenced through a
|
||||||
// stale memo.
|
// stale memo.
|
||||||
Uint64 m_evictionEpoch = 1;
|
//
|
||||||
|
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||||
|
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
||||||
|
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||||
|
// factory restarting at a dead factory's epoch value would honor its
|
||||||
|
// dangling entry pointers. The constructor takes a value strictly greater
|
||||||
|
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||||
|
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||||
|
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||||
|
static inline Uint64 s_evictionEpochSource = 0;
|
||||||
|
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||||
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
|
||||||
|
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
|
||||||
|
// invalid unless the buffer was created with this bit. Nothing asked for it until
|
||||||
|
// imageBuffer support existed, so the omission was invisible.
|
||||||
|
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||||
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||||
@@ -298,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.requiredFlags = requiredFlags,
|
.requiredFlags = requiredFlags,
|
||||||
});
|
});
|
||||||
if (!created || resource.buffer.Map() == nullptr) {
|
if (!created || resource.buffer.Map() == nullptr) {
|
||||||
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||||
static_cast<unsigned long long>(size));
|
static_cast<unsigned long long>(size));
|
||||||
resource.buffer.Destroy();
|
resource.buffer.Destroy();
|
||||||
resource.storageSize = 0;
|
resource.storageSize = 0;
|
||||||
@@ -320,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||||
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||||
resource.pendingFullUpload = true;
|
resource.pendingFullUpload = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -379,6 +383,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
BumpSliceEpoch(*resource);
|
BumpSliceEpoch(*resource);
|
||||||
// Any cached streaming slice refers to the previous contents.
|
// Any cached streaming slice refers to the previous contents.
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
|
// Redefining the store hands any adopted mapping back to the CPU shadow
|
||||||
|
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
|
||||||
|
// is an ordinary resident one again: it needs the busy-tracking and conditional
|
||||||
|
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
|
||||||
|
// store rather than hand back a mapping of the old one.
|
||||||
|
resource->persistentMapped = false;
|
||||||
if (!resource->buffer.IsValid()) {
|
if (!resource->buffer.IsValid()) {
|
||||||
return; // streaming-only resource: shadow + serial are enough
|
return; // streaming-only resource: shadow + serial are enough
|
||||||
}
|
}
|
||||||
@@ -399,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||||
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
|
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||||
resource->pendingFullUpload = true;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!IsResourceBusy(*resource)) {
|
if (!IsResourceBusy(*resource)) {
|
||||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||||
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
|
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||||
resource->pendingFullUpload = true;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -461,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
||||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||||
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
|
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||||
resource->pendingFullUpload = true;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -553,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
||||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||||
resource->buffer.Destroy();
|
resource->buffer.Destroy();
|
||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->usageFlags = 0;
|
resource->usageFlags = 0;
|
||||||
@@ -610,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case BufferKind::Uniform:
|
case BufferKind::Uniform:
|
||||||
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
||||||
case BufferKind::TextureBuffer:
|
case BufferKind::TextureBuffer:
|
||||||
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
|
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
|
||||||
|
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
|
||||||
|
// the two it is only becomes known when a shader that uses it is bound - long after
|
||||||
|
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
|
||||||
|
// invalid unless the buffer was created with the storage bit, so a buffer that
|
||||||
|
// acquired only the uniform bit could never be given one.
|
||||||
|
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
|
||||||
case BufferKind::ShaderStorage:
|
case BufferKind::ShaderStorage:
|
||||||
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
||||||
case BufferKind::Indirect:
|
case BufferKind::Indirect:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkResult result =
|
const VkResult result =
|
||||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||||
m_allocator = nullptr;
|
m_allocator = nullptr;
|
||||||
m_buffer = VK_NULL_HANDLE;
|
m_buffer = VK_NULL_HANDLE;
|
||||||
m_allocation = nullptr;
|
m_allocation = nullptr;
|
||||||
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
||||||
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||||
m_mappedData = nullptr;
|
m_mappedData = nullptr;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool wasMapped = IsMapped();
|
const Bool wasMapped = IsMapped();
|
||||||
void* mapped = wasMapped ? m_mappedData : Map();
|
void* mapped = wasMapped ? m_mappedData : Map();
|
||||||
if (mapped == nullptr) {
|
if (mapped == nullptr) {
|
||||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||||
if (flushResult != VK_SUCCESS) {
|
if (flushResult != VK_SUCCESS) {
|
||||||
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||||
if (!wasMapped) {
|
if (!wasMapped) {
|
||||||
Unmap();
|
Unmap();
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -166,7 +166,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||||
dst.mask |= src.mask;
|
dst.mask |= src.mask;
|
||||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
|
// The whole colour story travels together (same rule as
|
||||||
|
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||||
|
// payload carries its value in colorInt/colorUint and its branch selector
|
||||||
|
// in colorEncoding - dropping them here would leave the pending clear
|
||||||
|
// reading as an all-zero float one.
|
||||||
dst.color = src.color;
|
dst.color = src.color;
|
||||||
|
dst.colorEncoding = src.colorEncoding;
|
||||||
|
dst.colorInt = src.colorInt;
|
||||||
|
dst.colorUint = src.colorUint;
|
||||||
}
|
}
|
||||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
dst.depth = src.depth;
|
dst.depth = src.depth;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!attachment.IsComplete()) {
|
if (!attachment.IsComplete()) {
|
||||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||||
drawBufferIndex,
|
drawBufferIndex,
|
||||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||||
fbo.GetExternalIndex());
|
fbo.GetExternalIndex());
|
||||||
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto* texture = attachment.GetTexture().get();
|
auto* texture = attachment.GetTexture().get();
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||||
drawBufferIndex,
|
drawBufferIndex,
|
||||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||||
fbo.GetExternalIndex());
|
fbo.GetExternalIndex());
|
||||||
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
||||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||||
renderbuffer->GetSamples(),
|
renderbuffer->GetSamples(),
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||||
&imageFormatProperties);
|
&imageFormatProperties);
|
||||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
||||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||||
static_cast<Int>(format),
|
static_cast<Int>(format),
|
||||||
static_cast<Int>(sampleCount),
|
static_cast<Int>(sampleCount),
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
@@ -831,6 +831,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
||||||
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
||||||
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
||||||
|
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
||||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||||
@@ -855,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
||||||
m_rpFastValid = true;
|
m_rpFastValid = true;
|
||||||
m_rpFastFbo = &fbo;
|
m_rpFastFbo = &fbo;
|
||||||
|
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
||||||
m_rpFastFboVersion = fbo.GetObjectVersion();
|
m_rpFastFboVersion = fbo.GetObjectVersion();
|
||||||
m_rpFastSwapchainIndex = swapchainImageIndex;
|
m_rpFastSwapchainIndex = swapchainImageIndex;
|
||||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||||
@@ -929,7 +931,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||||
continue;
|
continue;
|
||||||
@@ -1105,7 +1107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||||
|
|
||||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||||
"using LOAD_OP_DONT_CARE",
|
"using LOAD_OP_DONT_CARE",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||||
@@ -1161,7 +1163,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||||
if (hasDistinctDepthAndStencilAttachments) {
|
if (hasDistinctDepthAndStencilAttachments) {
|
||||||
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||||
fbo.GetExternalIndex());
|
fbo.GetExternalIndex());
|
||||||
}
|
}
|
||||||
if (selectedDepthStencilAttachment != nullptr) {
|
if (selectedDepthStencilAttachment != nullptr) {
|
||||||
@@ -1223,7 +1225,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
||||||
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||||
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||||
depthAttachmentId);
|
depthAttachmentId);
|
||||||
}
|
}
|
||||||
@@ -1507,7 +1509,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ClearAttachmentPayload clearPayload{};
|
ClearAttachmentPayload clearPayload{};
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||||
if (pending.hasInlinePayload) {
|
if (pending.hasInlinePayload) {
|
||||||
|
// The inline payload was snapshotted when the entry was CREATED, but the
|
||||||
|
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
||||||
|
// glClear would replay the creation-time value and drop the new one (the
|
||||||
|
// texture path below is immune because it re-reads the live payload).
|
||||||
|
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
||||||
|
// pending clear, fall back to the snapshot only when none is queued.
|
||||||
|
if (s_renderPassManager != nullptr &&
|
||||||
|
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
||||||
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
||||||
|
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
||||||
|
3) {
|
||||||
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||||
|
ForceOpaqueClearAlpha(clearPayload);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
clearPayload = pending.inlinePayload;
|
clearPayload = pending.inlinePayload;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pending.key.texture == nullptr ||
|
if (pending.key.texture == nullptr ||
|
||||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||||
|
|||||||
@@ -289,6 +289,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||||
Bool m_rpFastValid = false;
|
Bool m_rpFastValid = false;
|
||||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||||
|
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||||
|
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||||
|
// the same number of version bumps would otherwise compare equal (both count
|
||||||
|
// from 0), serving the dead framebuffer's pass to the new object.
|
||||||
|
Uint64 m_rpFastFboLifetimeId = 0;
|
||||||
Uint16 m_rpFastFboVersion = 0;
|
Uint16 m_rpFastFboVersion = 0;
|
||||||
Uint32 m_rpFastSwapchainIndex = 0;
|
Uint32 m_rpFastSwapchainIndex = 0;
|
||||||
Uint64 m_rpFastTexEpoch = 0;
|
Uint64 m_rpFastTexEpoch = 0;
|
||||||
|
|||||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||||
|
|
||||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||||
oldResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||||
|
|
||||||
Vector<VkImageCopy> copyRegions;
|
Vector<VkImageCopy> copyRegions;
|
||||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||||
|
|
||||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||||
@@ -950,7 +947,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect);
|
const VkImageAspectFlags sampledAspect =
|
||||||
|
ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode());
|
||||||
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
||||||
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
||||||
if (perMipSampledView == VK_NULL_HANDLE) {
|
if (perMipSampledView == VK_NULL_HANDLE) {
|
||||||
@@ -974,13 +972,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return resource->sampledView;
|
return resource->sampledView;
|
||||||
}
|
}
|
||||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||||
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format));
|
static_cast<Int>(resource->format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1000,7 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||||
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||||
"for textureId=%d (available=0x%x)",
|
"for textureId=%d (available=0x%x)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
@@ -1014,7 +1012,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
if (view == VK_NULL_HANDLE) {
|
if (view == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1042,14 +1040,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
format = resource->format;
|
format = resource->format;
|
||||||
}
|
}
|
||||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||||
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format));
|
static_cast<Int>(resource->format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (format != resource->format &&
|
if (format != resource->format &&
|
||||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1069,7 +1067,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
break;
|
break;
|
||||||
case VK_IMAGE_VIEW_TYPE_3D:
|
case VK_IMAGE_VIEW_TYPE_3D:
|
||||||
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||||
__func__, texture.GetExternalIndex());
|
__func__, texture.GetExternalIndex());
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
default:
|
default:
|
||||||
@@ -1078,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
if (viewType != resource->viewType) {
|
if (viewType != resource->viewType) {
|
||||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||||
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -1113,7 +1111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||||
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||||
"(available=0x%x)",
|
"(available=0x%x)",
|
||||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
@@ -1124,7 +1122,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||||
if (view == VK_NULL_HANDLE) {
|
if (view == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1190,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool lowerTransitioned = TransitionImageLayout(
|
const Bool lowerTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
resource.aspect, 0, writtenMipLevel);
|
||||||
MOBILEGL_ASSERT(lowerTransitioned,
|
MOBILEGL_ASSERT(lowerTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1202,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool upperTransitioned = TransitionImageLayout(
|
const Bool upperTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||||
resource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(upperTransitioned,
|
MOBILEGL_ASSERT(upperTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1222,7 +1219,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1256,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||||
s_sampledReadStages, srcAccessMask,
|
s_sampledReadStages, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||||
resource->arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
StampResourceRecordingUse(*resource);
|
StampResourceRecordingUse(*resource);
|
||||||
@@ -1287,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
resource->aspect, 0, resource->mipLevels);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
@@ -1295,6 +1291,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
SamplerNumericDomain numericDomain,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask,
|
||||||
|
SampledTextureSnapshot& outSnapshot) {
|
||||||
|
outSnapshot = {};
|
||||||
|
TextureResource* source = SyncTextureAndGetDescriptor(texture);
|
||||||
|
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||||
|
source->sampledLevelCount == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
|
||||||
|
if (sampledFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
|
||||||
|
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sampledFormat != source->format &&
|
||||||
|
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
|
||||||
|
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
switch (source->viewType) {
|
||||||
|
case VK_IMAGE_VIEW_TYPE_1D:
|
||||||
|
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||||
|
imageType = VK_IMAGE_TYPE_1D;
|
||||||
|
break;
|
||||||
|
case VK_IMAGE_VIEW_TYPE_3D:
|
||||||
|
imageType = VK_IMAGE_TYPE_3D;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureResource snapshot{};
|
||||||
|
VkImageCreateInfo imageInfo{};
|
||||||
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
imageInfo.flags = source->imageCreateFlags;
|
||||||
|
imageInfo.imageType = imageType;
|
||||||
|
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
|
||||||
|
imageInfo.mipLevels = source->mipLevels;
|
||||||
|
imageInfo.arrayLayers = source->arrayLayers;
|
||||||
|
imageInfo.format = source->format;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
|
||||||
|
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
|
||||||
|
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
|
||||||
|
Vector<VkFormat> viewFormats;
|
||||||
|
VkImageFormatListCreateInfo formatListInfo{};
|
||||||
|
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
|
viewFormats.push_back(source->format);
|
||||||
|
if (sampledFormat != source->format) {
|
||||||
|
viewFormats.push_back(sampledFormat);
|
||||||
|
}
|
||||||
|
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||||
|
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||||
|
formatListInfo.pViewFormats = viewFormats.data();
|
||||||
|
imageInfo.pNext = &formatListInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
VmaAllocationCreateInfo allocationInfo{};
|
||||||
|
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||||
|
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||||
|
const VkResult createResult =
|
||||||
|
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
|
||||||
|
if (createResult != VK_SUCCESS) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
|
||||||
|
texture.GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.extent = source->extent;
|
||||||
|
snapshot.depth = source->depth;
|
||||||
|
snapshot.arrayLayers = source->arrayLayers;
|
||||||
|
snapshot.mipLevels = source->mipLevels;
|
||||||
|
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
|
||||||
|
snapshot.sampledLevelCount = source->sampledLevelCount;
|
||||||
|
snapshot.format = source->format;
|
||||||
|
snapshot.aspect = source->aspect;
|
||||||
|
snapshot.viewType = source->viewType;
|
||||||
|
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
snapshot.imageCreateFlags = imageInfo.flags;
|
||||||
|
snapshot.usageFlags = imageInfo.usage;
|
||||||
|
|
||||||
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
|
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||||
|
const VkImageAspectFlags sampledAspect =
|
||||||
|
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
|
||||||
|
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
|
||||||
|
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
|
||||||
|
snapshot.arrayLayers, &sampledComponents);
|
||||||
|
if (snapshot.sampledView == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
VkAccessFlags sourceAccessMask = 0;
|
||||||
|
const VkImageLayout sourceLayout = source->layout;
|
||||||
|
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
|
||||||
|
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
|
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
|
||||||
|
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
|
||||||
|
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||||
|
snapshot.sampledLevelCount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<VkImageCopy> copyRegions;
|
||||||
|
copyRegions.reserve(snapshot.sampledLevelCount);
|
||||||
|
for (Uint32 level = snapshot.sampledBaseMipLevel;
|
||||||
|
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
|
||||||
|
VkImageCopy copy{};
|
||||||
|
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
|
||||||
|
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
|
||||||
|
copy.extent = {std::max(source->extent.width >> level, 1u),
|
||||||
|
std::max(source->extent.height >> level, 1u),
|
||||||
|
std::max(source->depth >> level, 1u)};
|
||||||
|
copyRegions.push_back(copy);
|
||||||
|
}
|
||||||
|
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
||||||
|
|
||||||
|
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
|
||||||
|
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
|
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
|
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||||
|
snapshot.sampledLevelCount) ||
|
||||||
|
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
|
||||||
|
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
|
source->aspect, 0, source->mipLevels)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StampResourceRecordingUse(*source);
|
||||||
|
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
|
||||||
|
DeferResourceRelease(Move(snapshot));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||||
}
|
}
|
||||||
@@ -1346,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||||
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||||
|
resource.syncedShapeVersion != texture.GetShapeVersion() ||
|
||||||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||||
resource.syncedMipLevelCount != mipLevelCount;
|
resource.syncedMipLevelCount != mipLevelCount;
|
||||||
}
|
}
|
||||||
@@ -1354,8 +1503,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||||
Uint32 layerCount) {
|
Uint32 levelCount) {
|
||||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||||
@@ -1380,7 +1529,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||||
barrier.subresourceRange.levelCount = levelCount;
|
barrier.subresourceRange.levelCount = levelCount;
|
||||||
barrier.subresourceRange.baseArrayLayer = 0;
|
barrier.subresourceRange.baseArrayLayer = 0;
|
||||||
barrier.subresourceRange.layerCount = layerCount;
|
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||||
|
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||||
|
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||||
|
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||||
|
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||||
|
// validation layer warns about that literal 1 by name.
|
||||||
|
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||||
|
|
||||||
trackedLayout = newLayout;
|
trackedLayout = newLayout;
|
||||||
@@ -1439,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
TextureResource &outResource) {
|
TextureResource &outResource) {
|
||||||
// Cross-draw fast path: if the resource is already built and neither the texture's
|
// Cross-draw fast path: if the resource is already built and neither the texture's
|
||||||
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last
|
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
|
||||||
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
|
// nor its params changed since the last sync, there is nothing to re-check or
|
||||||
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
|
// re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
|
||||||
// maintained separately by the transition path, so the resource still reflects truth.
|
// the per-level dirty scan. Layout is maintained separately by the transition path, so
|
||||||
|
// the resource still reflects truth. The shape version is NOT redundant with the
|
||||||
|
// content one: glTexImage2D(..., nullptr) re-specifies a level's size or format
|
||||||
|
// without dirtying a texel, which is exactly how a re-specified image-unit texture used
|
||||||
|
// to keep reporting its old imageSize().
|
||||||
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
||||||
|
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
|
||||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 syncingMipLevelCount =
|
const Uint32 syncingMipLevelCount =
|
||||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||||
@@ -1455,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||||
outResource.syncedContentVersion == syncingContentVersion &&
|
outResource.syncedContentVersion == syncingContentVersion &&
|
||||||
|
outResource.syncedShapeVersion == syncingShapeVersion &&
|
||||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1475,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves
|
||||||
|
// out of the height it occupies GL-side and into z, which is the slot
|
||||||
|
// TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves
|
||||||
|
// extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have.
|
||||||
|
texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize);
|
||||||
|
|
||||||
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
||||||
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
||||||
return false;
|
return false;
|
||||||
@@ -1506,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!hasDirtyMipLevel) {
|
if (!hasDirtyMipLevel) {
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
|
outResource.syncedShapeVersion = syncingShapeVersion;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1515,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
|
outResource.syncedShapeVersion = syncingShapeVersion;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1573,7 +1742,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||||
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||||
// assertion would take the whole process down instead.
|
// assertion would take the whole process down instead.
|
||||||
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||||
"mipLevels=%u vkViewType=%d",
|
"mipLevels=%u vkViewType=%d",
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
||||||
@@ -1694,6 +1863,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) {
|
||||||
|
// Nothing at two samples or above. Reachable because the frontend validates
|
||||||
|
// multisample allocations against the count MobileGL ADVERTISES (GL requires
|
||||||
|
// GL_MAX_SAMPLES >= 4) rather than against the device's per-format support, so
|
||||||
|
// a format this device cannot multisample at all now gets here instead of
|
||||||
|
// being refused up front. Keeping the unsupported count would hand
|
||||||
|
// vkCreateImage an invalid VkImageCreateInfo; one sample is at least a legal
|
||||||
|
// image, and the samples-08726 hazard above is the lesser of the two.
|
||||||
|
MGLOG_W_ONCE("Multisample texture format %d supports no count above one on this device; "
|
||||||
|
"backing it with a single sample",
|
||||||
|
static_cast<Int>(format));
|
||||||
|
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
|
||||||
|
}
|
||||||
if (rounded != 0) {
|
if (rounded != 0) {
|
||||||
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||||
}
|
}
|
||||||
@@ -1802,7 +1984,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Losing reinterpreted views only degrades the formatless-image feature for
|
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||||
// this texture; failing creation would lose the texture entirely, so retry
|
// this texture; failing creation would lose the texture entirely, so retry
|
||||||
// as a plain immutable-format image.
|
// as a plain immutable-format image.
|
||||||
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||||
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||||
"will be unavailable for it)",
|
"will be unavailable for it)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||||
@@ -1820,7 +2002,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||||
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||||
"unavailable for it)",
|
"unavailable for it)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||||
@@ -1839,6 +2021,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
texture.GetExternalIndex(),
|
texture.GetExternalIndex(),
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||||
|
// The preserved image was written by GPU work that may still be in flight
|
||||||
|
// (preserve requires layout != UNDEFINED); park it on the deferred ring
|
||||||
|
// like every other destruction path instead of letting the unique_ptr
|
||||||
|
// destroy it synchronously under the GPU.
|
||||||
|
if (preservedResource) {
|
||||||
|
DeferResourceRelease(Move(*preservedResource));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1852,13 +2041,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkResult createImageResult =
|
const VkResult createImageResult =
|
||||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||||
if (createImageResult != VK_SUCCESS) {
|
if (createImageResult != VK_SUCCESS) {
|
||||||
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
|
||||||
|
// recovers from, and it re-fires on every sync of every texture the driver refuses.
|
||||||
|
MGLOG_E_ONCE("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||||
"mips=%u samples=%d format=%d",
|
"mips=%u samples=%d format=%d",
|
||||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||||
resource.image = VK_NULL_HANDLE;
|
resource.image = VK_NULL_HANDLE;
|
||||||
resource.allocation = nullptr;
|
resource.allocation = nullptr;
|
||||||
|
// Same as the probe failure above: the preserved live image must go through
|
||||||
|
// the deferred ring, never a synchronous destructor while frames that
|
||||||
|
// reference it are still in flight.
|
||||||
|
if (preservedResource) {
|
||||||
|
DeferResourceRelease(Move(*preservedResource));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||||
@@ -2238,7 +2435,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource.fullView == VK_NULL_HANDLE) {
|
if (resource.fullView == VK_NULL_HANDLE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect);
|
const VkImageAspectFlags sampledAspect =
|
||||||
|
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||||
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
|
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
|
||||||
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
||||||
if (resource.sampledView == VK_NULL_HANDLE) {
|
if (resource.sampledView == VK_NULL_HANDLE) {
|
||||||
@@ -2353,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
uploadItem.target = target;
|
uploadItem.target = target;
|
||||||
uploadItem.level = level;
|
uploadItem.level = level;
|
||||||
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
||||||
uploadItem.texelSize = texelSize;
|
// Vulkan geometry, like the image this stages into (see SyncTexture): a 1D
|
||||||
|
// array's layers move from y to z, where the copy loop's depthSelectsArrayLayer
|
||||||
|
// branch turns them into layerCount. The shadow needs no repacking to follow -
|
||||||
|
// one layer of a 1D array IS one row of `width` texels, so the tight-packed
|
||||||
|
// per-layer copy the swapped size describes reads the same bytes in the same
|
||||||
|
// order as the row-major level it replaces.
|
||||||
|
uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize);
|
||||||
uploadItem.source = source;
|
uploadItem.source = source;
|
||||||
uploadItem.offset = stagingSize;
|
uploadItem.offset = stagingSize;
|
||||||
uploadItem.uploadByteSize = byteSize;
|
uploadItem.uploadByteSize = byteSize;
|
||||||
@@ -2391,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
||||||
}
|
}
|
||||||
|
// The boxes came out of the shadow in GL coordinates, where a 1D
|
||||||
|
// array's layer is the y. They have to follow texelSize across to z or
|
||||||
|
// they would address rows of an image that now has exactly one, and
|
||||||
|
// the staging walk would read the wrong bytes for them. Every byte
|
||||||
|
// count computed above is a product of the three extents, so moving
|
||||||
|
// the axes leaves all of them alone - and an OFFSET lands on a zero y,
|
||||||
|
// not on the extent's one, which is why this is spelled out rather than
|
||||||
|
// handed to ToVulkanLevelExtent.
|
||||||
|
if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) {
|
||||||
|
uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()};
|
||||||
|
uploadItem.regionSize = {uploadItem.regionSize.x(), 1,
|
||||||
|
uploadItem.regionSize.y()};
|
||||||
|
for (auto& rect : uploadItem.rects) {
|
||||||
|
rect.lo = {rect.lo.x(), 0, rect.lo.y()};
|
||||||
|
rect.hi = {rect.hi.x(), 1, rect.hi.y()};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (formatInfo.expandRgbToRgba) {
|
if (formatInfo.expandRgbToRgba) {
|
||||||
@@ -2424,7 +2645,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
if (!srcIsD24S8 && !srcIsD32FS8) {
|
||||||
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
MGLOG_E_ONCE("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||||
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||||
for (const auto& item : uploadItems) {
|
for (const auto& item : uploadItems) {
|
||||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||||
@@ -2601,7 +2822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
uploadSrcAccessMask,
|
uploadSrcAccessMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
aspectMask, 0, outResource.mipLevels);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||||
|
|
||||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||||
@@ -2705,7 +2926,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
s_sampledReadStages,
|
s_sampledReadStages,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
VK_ACCESS_SHADER_READ_BIT,
|
VK_ACCESS_SHADER_READ_BIT,
|
||||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
aspectMask, 0, outResource.mipLevels);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||||
outResource.layout = finalLayout;
|
outResource.layout = finalLayout;
|
||||||
|
|
||||||
@@ -2860,10 +3081,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) {
|
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||||
|
GLenum depthStencilTextureMode) {
|
||||||
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
return VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
}
|
}
|
||||||
|
// A sampled view of a combined depth/stencil image may name exactly one aspect
|
||||||
|
// (VUID-VkDescriptorImageInfo-imageView-01976), and GL_DEPTH_STENCIL_TEXTURE_MODE is
|
||||||
|
// what picks it - the whole content of GL_ARB_stencil_texturing. Depth stays the
|
||||||
|
// default, so nothing that never sets the mode changes shape. The texture's params
|
||||||
|
// version moves with the mode, which is what makes the cached views be rebuilt.
|
||||||
|
if (depthStencilTextureMode == GL_STENCIL_INDEX && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) {
|
||||||
|
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
|
}
|
||||||
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
|
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
|
||||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ class ITextureObject;
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
enum class SamplerNumericDomain : Uint8;
|
enum class SamplerNumericDomain : Uint8;
|
||||||
|
|
||||||
|
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
||||||
|
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
||||||
|
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
||||||
|
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
|
||||||
|
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
|
||||||
|
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
|
||||||
|
// level size into Vulkan image geometry has to move the count across first, and every GL-space
|
||||||
|
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
|
||||||
|
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
|
||||||
|
//
|
||||||
|
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
|
||||||
|
// depth/layer count in z, which is where the Vulkan side expects it.
|
||||||
|
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
|
||||||
|
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||||
|
return {glTexelSize.x(), 1, glTexelSize.y()};
|
||||||
|
}
|
||||||
|
return glTexelSize;
|
||||||
|
}
|
||||||
|
|
||||||
class VkTextureManager {
|
class VkTextureManager {
|
||||||
public:
|
public:
|
||||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||||
@@ -206,6 +225,12 @@ public:
|
|||||||
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
||||||
// busts the skip even if it failed to bump the content version.
|
// busts the skip even if it failed to bump the content version.
|
||||||
Uint32 syncedMipLevelCount = 0;
|
Uint32 syncedMipLevelCount = 0;
|
||||||
|
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
|
||||||
|
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
|
||||||
|
// already-defined level changes its size or format and dirties no texel, so it moves the
|
||||||
|
// shape version and nothing else. Without this in the early-out key the image, its views
|
||||||
|
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
|
||||||
|
Uint64 syncedShapeVersion = 0;
|
||||||
|
|
||||||
TextureResource() = default;
|
TextureResource() = default;
|
||||||
TextureResource(const TextureResource&) = delete;
|
TextureResource(const TextureResource&) = delete;
|
||||||
@@ -237,6 +262,7 @@ public:
|
|||||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
|
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reset() {
|
void Reset() {
|
||||||
@@ -300,6 +326,7 @@ public:
|
|||||||
syncedTextureParamsVersion = 0;
|
syncedTextureParamsVersion = 0;
|
||||||
syncedContentVersion = 0;
|
syncedContentVersion = 0;
|
||||||
syncedMipLevelCount = 0;
|
syncedMipLevelCount = 0;
|
||||||
|
syncedShapeVersion = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
~TextureResource() {
|
~TextureResource() {
|
||||||
@@ -310,6 +337,11 @@ public:
|
|||||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SampledTextureSnapshot {
|
||||||
|
VkImageView imageView = VK_NULL_HANDLE;
|
||||||
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
};
|
||||||
|
|
||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
@@ -343,6 +375,13 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
|
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||||
|
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||||
|
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||||
|
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||||
|
SamplerNumericDomain numericDomain,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask,
|
||||||
|
SampledTextureSnapshot& outSnapshot);
|
||||||
|
|
||||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||||
// generation advances every time the frame command buffer (re)begins
|
// generation advances every time the frame command buffer (re)begins
|
||||||
@@ -379,17 +418,33 @@ public:
|
|||||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||||
|
|
||||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
|
||||||
|
// anything for an image that carries both aspects. Defaulted so the call sites that have no
|
||||||
|
// texture in hand keep the depth-aspect answer they have always given.
|
||||||
|
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||||
|
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
|
||||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
|
|
||||||
|
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||||
|
//
|
||||||
|
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||||
|
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||||
|
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||||
|
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||||
|
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||||
|
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||||
|
//
|
||||||
|
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||||
|
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||||
|
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||||
|
// Nothing does, or can, do that per layer.
|
||||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||||
Uint32 layerCount = 1);
|
|
||||||
|
|
||||||
SizeT CollectGarbage();
|
SizeT CollectGarbage();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||||
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||||
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
||||||
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||||
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& poolState : m_pools) {
|
for (auto& poolState : m_pools) {
|
||||||
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||||
Shutdown();
|
Shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& poolState = m_pools[frameIndex];
|
auto& poolState = m_pools[frameIndex];
|
||||||
if (poolState.cursor >= m_slotsPerPool) {
|
if (poolState.cursor >= m_slotsPerPool) {
|
||||||
if (!poolState.exhaustionWarned) {
|
if (!poolState.exhaustionWarned) {
|
||||||
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||||
"this frame fall back to the frontend path",
|
"this frame fall back to the frontend path",
|
||||||
frameIndex, m_slotsPerPool);
|
frameIndex, m_slotsPerPool);
|
||||||
poolState.exhaustionWarned = true;
|
poolState.exhaustionWarned = true;
|
||||||
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
||||||
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||||
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
||||||
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (resultWithAvailability[1] == 0) {
|
if (resultWithAvailability[1] == 0) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
#include "VkTimerQueryManager.h"
|
#include "VkTimerQueryManager.h"
|
||||||
#include "MG_Util/Math/VectorTypes.h"
|
#include "MG_Util/Math/VectorTypes.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
@@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLbitfield mask, GLenum filter);
|
GLbitfield mask, GLenum filter);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height);
|
GLint x, GLint y, GLsizei width, GLsizei height);
|
||||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
const CopyImageEndpoint& dstEndpoint,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
@@ -216,10 +217,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
||||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
||||||
// the way back, exactly as the colour ReadPixels path does.
|
// the way back, exactly as the colour ReadPixels path does.
|
||||||
|
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
|
||||||
|
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
|
||||||
|
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
|
||||||
|
// only the copy region differs; everything after it is written against `height`.
|
||||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels, Bool defaultFramebufferOrientation = false);
|
void* pixels, Bool defaultFramebufferOrientation = false,
|
||||||
|
Uint32 sourceLayerCount = 1);
|
||||||
// Same-extent depth blit between images of different depth formats: host
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
@@ -229,6 +235,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||||
|
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||||
|
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||||
|
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||||
|
VkExtent2D imageExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||||
|
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||||
|
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||||
|
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||||
|
Uint32 logicalHeight,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
SizeT texelSize, Uint8* outPixels);
|
||||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
GLenum destinationType, SizeT destinationRowStride,
|
GLenum destinationType, SizeT destinationRowStride,
|
||||||
@@ -298,6 +316,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||||
// honored rather than accepted-and-ignored.
|
// honored rather than accepted-and-ignored.
|
||||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||||
|
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||||
|
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||||
|
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||||
|
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||||
|
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||||
|
}
|
||||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||||
@@ -536,7 +560,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
|
// Native subgroup topology, queried at device creation for the compute-module
|
||||||
|
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
||||||
|
// stage flag; 0 / false when the device has no usable compute subgroups or
|
||||||
|
// MOBILEGL_DISABLE_SUBGROUP forced them off.
|
||||||
|
Uint32 m_nativeSubgroupSize = 0;
|
||||||
|
Bool m_nativeSubgroupSupported = false;
|
||||||
|
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||||
|
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
|
||||||
|
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
|
||||||
|
Uint32 m_maxComputeWorkgroupSubgroups = 0;
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
|
// Set only after descriptor-indexing feature AND property queries prove that
|
||||||
|
// update-after-bind is legal for every descriptor category this renderer emits.
|
||||||
|
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||||
// drive a runtime fallback when the device lacks them.
|
// drive a runtime fallback when the device lacks them.
|
||||||
@@ -547,6 +584,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||||
|
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||||
|
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||||
|
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||||
|
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||||
|
Bool m_multiViewportFeatureEnabled = false;
|
||||||
|
Uint32 m_maxRasterizableViewports = 1;
|
||||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||||
@@ -770,6 +813,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 m_lastLodProgramVersion = 0;
|
Uint32 m_lastLodProgramVersion = 0;
|
||||||
Uint64 m_lastLodBindGeneration = 0;
|
Uint64 m_lastLodBindGeneration = 0;
|
||||||
Uint64 m_lastLodParamsSum = 0;
|
Uint64 m_lastLodParamsSum = 0;
|
||||||
|
// Sampling-resolution generation at probe time. The probe reads the effective
|
||||||
|
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
|
||||||
|
// the params-version sum above never moves for them.
|
||||||
|
Uint64 m_lastLodSamplingGeneration = 0;
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||||
|
|
||||||
@@ -807,6 +854,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 vaoLifetimeId = 0;
|
Uint64 vaoLifetimeId = 0;
|
||||||
Uint32 vaoConfigVersion = 0;
|
Uint32 vaoConfigVersion = 0;
|
||||||
const void* drawFbo = nullptr;
|
const void* drawFbo = nullptr;
|
||||||
|
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||||
|
// deleted FBO recycled at the same address with the same fresh version
|
||||||
|
// count would otherwise compare equal (same ABA as the render-pass
|
||||||
|
// manager's fast-path memo).
|
||||||
|
Uint64 drawFboLifetimeId = 0;
|
||||||
Uint16 fboVersion = 0;
|
Uint16 fboVersion = 0;
|
||||||
Bool drawFboIsDefault = false;
|
Bool drawFboIsDefault = false;
|
||||||
Uint renderStateVersion = 0;
|
Uint renderStateVersion = 0;
|
||||||
@@ -830,6 +882,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// re-resolve just the pipeline against the active pass; a change that
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
// flips it must fall back to the full path's pass selection.
|
// flips it must fall back to the full path's pass selection.
|
||||||
Bool drawUsesDepthStencil = false;
|
Bool drawUsesDepthStencil = false;
|
||||||
|
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||||
|
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||||
|
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||||
|
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||||
|
Uint32 viewportCount = 1;
|
||||||
IntVec2 renderPassExtent = {0, 0};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||||
@@ -897,6 +954,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// already sampleable.
|
// already sampleable.
|
||||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
|
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
|
||||||
|
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||||
@@ -1023,6 +1082,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||||
VkDeviceSize indexSliceOffset = 0;
|
VkDeviceSize indexSliceOffset = 0;
|
||||||
Uint64 indexFrameSerial = 0;
|
Uint64 indexFrameSerial = 0;
|
||||||
|
// The EBO carried a host map when the slice was recorded - the mirror of
|
||||||
|
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
|
||||||
|
// persistent map mutates its shadow with no API call and no epoch bump, so
|
||||||
|
// the one-compare rescue must decline and re-run the acquire, whose
|
||||||
|
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
|
||||||
|
// is already covered: AcquirePersistentMap bumps the slice epoch for the
|
||||||
|
// request itself, adopted or declined.
|
||||||
|
Bool indexBufferMapped = false;
|
||||||
|
|
||||||
// Bound per draw (first bindingCount elements).
|
// Bound per draw (first bindingCount elements).
|
||||||
VkBuffer vkBuffers[kMaxBindings] = {};
|
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||||
@@ -1116,11 +1183,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
FrameContext::FrameData& frame,
|
FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
||||||
|
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
||||||
|
// storage descriptor continues to name the application texture.
|
||||||
|
Bool PrepareSamplerImageFeedbackSnapshots(
|
||||||
|
FrameContext::FrameData& frame,
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask);
|
||||||
|
|
||||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||||
|
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||||
|
// rasterizable viewport count, which takes the unmemoized array path.
|
||||||
|
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||||
|
Uint32 viewportCount = 1);
|
||||||
|
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||||
|
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||||
|
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||||
|
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||||
|
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||||
|
// disagree.
|
||||||
|
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||||
|
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||||
|
}
|
||||||
|
|
||||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
|
||||||
|
// shared by capability advertisement (BackendObject) and module lowering
|
||||||
|
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
|
||||||
|
//
|
||||||
|
// Native subgroups are the implementation whenever the device has them, whatever
|
||||||
|
// their width - subgroup operations execute on the hardware paths they were made
|
||||||
|
// for. Module-level repairs keep the GL contract intact around them:
|
||||||
|
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
||||||
|
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
||||||
|
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
|
||||||
|
// reusing that scratch, when explicitly enabled;
|
||||||
|
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
||||||
|
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
||||||
|
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
||||||
|
// devices with no subgroup support at all, and only when the user opts in with
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
|
||||||
|
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupFeatures =
|
||||||
|
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
||||||
|
|
||||||
|
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||||
|
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||||
|
!MG_Config::Features.DisableSubgroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||||
|
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||||
|
// grows one under-declared array; every other module passes through untouched.
|
||||||
|
return MG_Config::Features.FixIterationRPSubgroupScratch !=
|
||||||
|
MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldFixIterationRPBarrier() {
|
||||||
|
return MG_Config::Features.IterationRPFixBarrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldDeriveNumSubgroups() {
|
||||||
|
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
|
||||||
|
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
||||||
|
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||||
|
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
||||||
|
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
@@ -74,6 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||||
// call: appending its format to the base format while its arguments precede the base
|
// call: appending its format to the base format while its arguments precede the base
|
||||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||||
|
//
|
||||||
|
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
|
||||||
|
// MobileGL believes it has already made legal came back non-success, which is a
|
||||||
|
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
|
||||||
|
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
|
||||||
|
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
|
||||||
|
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
|
||||||
|
// the assert is compiled out by contract.
|
||||||
|
//
|
||||||
|
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
|
||||||
|
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
|
||||||
|
// where a driver legitimately refuses an image the format pre-check accepted.
|
||||||
#define VK_VERIFY(expr, ...) \
|
#define VK_VERIFY(expr, ...) \
|
||||||
do { \
|
do { \
|
||||||
VkResult _vk_verify_result = (expr); \
|
VkResult _vk_verify_result = (expr); \
|
||||||
|
|||||||
@@ -44,3 +44,5 @@ add_subdirectory(Program)
|
|||||||
add_subdirectory(Buffer)
|
add_subdirectory(Buffer)
|
||||||
add_subdirectory(Driver)
|
add_subdirectory(Driver)
|
||||||
add_subdirectory(Container)
|
add_subdirectory(Container)
|
||||||
|
add_subdirectory(ShaderCache)
|
||||||
|
add_subdirectory(Transpile)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
TranslationCacheBench
|
||||||
|
TranslationCacheBench.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(TranslationCacheBench PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
TranslationCacheBench PRIVATE
|
||||||
|
benchmark::benchmark
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME TranslationCacheBench COMMAND TranslationCacheBench --benchmark_counters_tabular=true)
|
||||||
|
set_tests_properties(TranslationCacheBench PROPERTIES LABELS benchmark)
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// What the two-level shader translation memo is worth, measured on the workload that
|
||||||
|
// motivated it: the KHR-GL33.texture_swizzle.smoke_* shape, where one case builds 2592
|
||||||
|
// programs out of a handful of distinct sources.
|
||||||
|
//
|
||||||
|
// Four pairs of cases, each Off/On:
|
||||||
|
//
|
||||||
|
// ProgramLink - the whole glCompileShader + glLinkProgram path for one program, with
|
||||||
|
// FRESH SHADER OBJECTS every iteration. This is the CTS shape exactly,
|
||||||
|
// and it is the headline case now. It used to be the PESSIMISTIC one:
|
||||||
|
// a hit still paid for both glslang parses, because the parse happens
|
||||||
|
// at glCompileShader - a different entry point from the one L1
|
||||||
|
// memoizes - and fresh shader objects meant ShaderCompileAdoptionMap
|
||||||
|
// could not hand the earlier parse over either. L1c is what closed
|
||||||
|
// that: the compile half of the memo recognises each stage's source
|
||||||
|
// and publishes its verdict without parsing, so on a hit this case now
|
||||||
|
// constructs no glslang object at all.
|
||||||
|
//
|
||||||
|
// SharedShaderLink - the same program population with the shader objects KEPT ALIVE, so
|
||||||
|
// the parses happen once outside the measured loop whatever the cache
|
||||||
|
// does. That makes it the CONTROL for L1c rather than a target: its
|
||||||
|
// numbers should not move, and if they do, L1c has added cost to a
|
||||||
|
// path it was supposed to leave alone.
|
||||||
|
//
|
||||||
|
// DeferredParseLink - the shape where L1c could LOSE: a constant vertex source (which
|
||||||
|
// hits L1c and therefore skips its parse) against a fresh fragment
|
||||||
|
// source every iteration (which makes the PROGRAM key miss, so the
|
||||||
|
// skipped parse has to happen inside the link after all). Same parse
|
||||||
|
// count either way, so the pair should land within noise; see its own
|
||||||
|
// header below.
|
||||||
|
//
|
||||||
|
// EsslTranspile - the DirectGLES backend segment: the SPIR-V pass chain plus
|
||||||
|
// SPIRV-Cross. Runs the driver-INDEPENDENT half of the real chain (the
|
||||||
|
// passes SyncToBackend runs unconditionally, plus the two stage-gated
|
||||||
|
// ones a fragment module reaches) so the miss path costs what
|
||||||
|
// production costs; the capability-gated passes need a live ES driver
|
||||||
|
// and are not reachable from a benchmark process.
|
||||||
|
//
|
||||||
|
// Every On case runs with a warm cache: the first iteration misses and every one after it
|
||||||
|
// hits, which is exactly the steady state of a 2592-program smoke case.
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
|
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char* kVertexSource = R"(#version 460
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
out vec3 vPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vPos = aPos;
|
||||||
|
vUv = aPos.xy * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Shaped after gl3cTextureSwizzleTests.cpp's template: a sampler of one type, one
|
||||||
|
// TEXTURE_ACCESS, one CHANNEL, and an output whose BASIC_TYPE is the only thing that
|
||||||
|
// varies within a case. Padded with enough real arithmetic that the translation chain
|
||||||
|
// is doing work rather than measuring fixed overheads.
|
||||||
|
// `padLines` = 0 is the honest CTS size: gl3cTextureSwizzleTests' smoke template is a
|
||||||
|
// handful of lines, and that is the workload the memo exists for. The padded variant is
|
||||||
|
// kept alongside it because a shaderpack stage is orders of magnitude bigger, and the
|
||||||
|
// two bracket the ratio the cache is worth in practice.
|
||||||
|
String SwizzleLikeFragment(const String& prefix, const int padLines) {
|
||||||
|
String source = "#version 460\n";
|
||||||
|
source += "in vec3 vPos;\n";
|
||||||
|
source += "in vec2 vUv;\n";
|
||||||
|
source += "layout(location = 0) out " + prefix + "vec4 fragColor;\n";
|
||||||
|
source += "uniform sampler2D uTex;\n";
|
||||||
|
source += "uniform vec4 uTint;\n";
|
||||||
|
source += "uniform mat4 uModel;\n";
|
||||||
|
source += "uniform float uArr[8];\n";
|
||||||
|
source += "void main() {\n";
|
||||||
|
source += " vec4 s = texture(uTex, vUv);\n";
|
||||||
|
source += " float acc = s.r;\n";
|
||||||
|
for (int i = 0; i < padLines; ++i) {
|
||||||
|
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
||||||
|
}
|
||||||
|
source += " for (int i = 0; i < 8; ++i) acc += uArr[i];\n";
|
||||||
|
source += " vec4 p = uModel * vec4(vPos, 1.0);\n";
|
||||||
|
source += " fragColor = " + prefix + "vec4((s + uTint) * acc + p);\n";
|
||||||
|
source += "}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheModeScope {
|
||||||
|
public:
|
||||||
|
explicit CacheModeScope(const Bool enabled)
|
||||||
|
: m_saved(MG_Config::Features.ShaderTranslationCache) {
|
||||||
|
MG_Config::Features.ShaderTranslationCache =
|
||||||
|
enabled ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~CacheModeScope() { MG_Config::Features.ShaderTranslationCache = m_saved; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MG_Config::QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SyncCompileScope {
|
||||||
|
public:
|
||||||
|
SyncCompileScope() : m_saved(MG_Config::Features.AsyncShaderCompile) {
|
||||||
|
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~SyncCompileScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MG_Config::QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One program, built the way the CTS builds one: fresh shader objects every time.
|
||||||
|
void LinkOneProgram(const String& vertexSource, const String& fragmentSource) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
const char* vsText = vertexSource.c_str();
|
||||||
|
ShaderSource(vs, 1, &vsText, nullptr);
|
||||||
|
CompileShader(vs);
|
||||||
|
|
||||||
|
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
const char* fsText = fragmentSource.c_str();
|
||||||
|
ShaderSource(fs, 1, &fsText, nullptr);
|
||||||
|
CompileShader(fs);
|
||||||
|
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, vs);
|
||||||
|
AttachShader(program, fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
benchmark::DoNotOptimize(program);
|
||||||
|
|
||||||
|
DeleteProgram(program);
|
||||||
|
DeleteShader(vs);
|
||||||
|
DeleteShader(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Uint32> BuildSanitizedFragmentSpirv(const String& fragmentSource) {
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
|
||||||
|
auto shader = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!shader) return {};
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shader.value()}};
|
||||||
|
auto program = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!program) return {};
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program.value()};
|
||||||
|
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!binary || binary->empty()) return {};
|
||||||
|
Vector<Uint32> sanitized;
|
||||||
|
if (!ShaderCompiler::SanitizeAndOptimizeBinary(binary->front(), sanitized)) return {};
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The driver-independent part of BackendProgramObjectImpl::TranspileSpirvToEssl, in the
|
||||||
|
// same order. What is missing is only the capability-gated passes (viewport lowering,
|
||||||
|
// multisample clamping, noperspective emulation, the image-format bake), which cannot
|
||||||
|
// fire without a live ES driver to arm them.
|
||||||
|
Bool TranspileLikeDirectGles(const Vector<Uint32>& spirv, const Uint esslVersion, String& outEssl) {
|
||||||
|
Vector<Uint32> a;
|
||||||
|
const Vector<Uint32>* effective = &spirv;
|
||||||
|
if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*effective, a, false) && !a.empty()) {
|
||||||
|
effective = &a;
|
||||||
|
}
|
||||||
|
Vector<Uint32> b;
|
||||||
|
if (ShaderCompiler::LowerRectImages(*effective, b, false) && !b.empty()) effective = &b;
|
||||||
|
Vector<Uint32> c;
|
||||||
|
if (ShaderCompiler::Lower1DArrayImagesForEssl(*effective, c, false) && !c.empty()) effective = &c;
|
||||||
|
Vector<Uint32> d;
|
||||||
|
if (ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*effective, d, false) && !d.empty()) {
|
||||||
|
effective = &d;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpvcSession session(*effective, SessionUsageBit::Transpile);
|
||||||
|
spvc_compiler_options options;
|
||||||
|
if (session.CreateOptions(&options) != SPVC_SUCCESS) return false;
|
||||||
|
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, esslVersion);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||||
|
session.SetOptions(options);
|
||||||
|
const char* result = nullptr;
|
||||||
|
session.Compile(&result);
|
||||||
|
if (!result) return false;
|
||||||
|
outEssl = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EsslTranslationKeyInputs EsslInputsFor(const Vector<Uint32>& spirv) {
|
||||||
|
EsslTranslationKeyInputs inputs;
|
||||||
|
inputs.spirv = &spirv;
|
||||||
|
inputs.shaderType = GL_FRAGMENT_SHADER;
|
||||||
|
inputs.maxColorTextureSamples = 4;
|
||||||
|
inputs.maxIntegerSamples = 1;
|
||||||
|
inputs.maxDepthTextureSamples = 4;
|
||||||
|
inputs.advertisedMaxSamples = 4;
|
||||||
|
inputs.esslVersion = 320;
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1, in situ: the full glCompileShader + glLinkProgram path for a repeated program.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// Arg(0) = the CTS smoke size; Arg(120) = a heavy stage, bracketing the ratio.
|
||||||
|
static void BM_ProgramLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, fs);
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_ProgramLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_ProgramLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
||||||
|
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, fs);
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
// Two stages per iteration, so a clean run shows L1c_hits == 2 * iterations and zero
|
||||||
|
// misses: every glCompileShader in the loop skipped its parse.
|
||||||
|
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
||||||
|
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
|
||||||
|
//
|
||||||
|
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
|
||||||
|
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
|
||||||
|
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
|
||||||
|
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
|
||||||
|
// leaves is the parse, not the link.
|
||||||
|
//
|
||||||
|
// This pair keeps the shader objects alive, so the parses happen once before the measured
|
||||||
|
// loop and the L1 hit then skips the link, mapIO, the SPIR-V, the reflection and the routing
|
||||||
|
// outright.
|
||||||
|
//
|
||||||
|
// SINCE L1c THIS IS THE CONTROL, NOT THE TARGET. Nothing inside the measured loop calls
|
||||||
|
// glCompileShader, so L1c cannot fire here at all - which is exactly what makes the pair
|
||||||
|
// useful: it is the shape that says whether the compile-side memo has slowed the LINK path
|
||||||
|
// down. Its numbers should be indistinguishable from the pre-L1c ones.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
namespace {
|
||||||
|
struct SharedShaders {
|
||||||
|
GLuint vs = 0;
|
||||||
|
GLuint fs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
SharedShaders shaders;
|
||||||
|
shaders.vs = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
const char* vsText = vertexSource.c_str();
|
||||||
|
ShaderSource(shaders.vs, 1, &vsText, nullptr);
|
||||||
|
CompileShader(shaders.vs);
|
||||||
|
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
const char* fsText = fragmentSource.c_str();
|
||||||
|
ShaderSource(shaders.fs, 1, &fsText, nullptr);
|
||||||
|
CompileShader(shaders.fs);
|
||||||
|
return shaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkFromSharedShaders(const SharedShaders& shaders) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, shaders.vs);
|
||||||
|
AttachShader(program, shaders.fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
benchmark::DoNotOptimize(program);
|
||||||
|
DeleteProgram(program);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const SharedShaders shaders =
|
||||||
|
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkFromSharedShaders(shaders);
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const SharedShaders shaders =
|
||||||
|
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkFromSharedShaders(shaders);
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
static void BM_EsslTranspile_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const Vector<Uint32> spirv =
|
||||||
|
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
if (spirv.empty()) {
|
||||||
|
state.SkipWithError("could not build the fragment module");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String essl;
|
||||||
|
for (auto _ : state) {
|
||||||
|
if (!TranspileLikeDirectGles(spirv, 320, essl)) {
|
||||||
|
state.SkipWithError("transpile failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(essl.data());
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_EsslTranspile_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_EsslTranspile_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const Vector<Uint32> spirv =
|
||||||
|
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
if (spirv.empty()) {
|
||||||
|
state.SkipWithError("could not build the fragment module");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BoundedTranslationCache<EsslTranslationResult> cache("bench L2", 64, 8u << 20);
|
||||||
|
const EsslTranslationKeyInputs inputs = EsslInputsFor(spirv);
|
||||||
|
for (auto _ : state) {
|
||||||
|
const TranslationCacheKey key = BuildEsslTranslationKey(inputs);
|
||||||
|
EsslTranslationResultPtr hit = cache.Find(key);
|
||||||
|
if (!hit) {
|
||||||
|
auto payload = MakeShared<EsslTranslationResult>();
|
||||||
|
if (!TranspileLikeDirectGles(spirv, inputs.esslVersion, payload->essl)) {
|
||||||
|
state.SkipWithError("transpile failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cache.Insert(key, EsslTranslationResultPtr(payload), EsslTranslationResultBytes(*payload));
|
||||||
|
hit = payload;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(hit->essl.data());
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = cache.Stats();
|
||||||
|
state.counters["L2_hits"] = static_cast<double>(stats.hits);
|
||||||
|
state.counters["L2_misses"] = static_cast<double>(stats.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_EsslTranspile_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1c, the shape where it could LOSE rather than win: the DEFERRED PARSE.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// A stage whose compile hits L1c holds no AST, so if the program-level key then MISSES, the
|
||||||
|
// parse it skipped has to happen anyway - inside the link, via ClaimParsedShader. The parse
|
||||||
|
// is moved, not removed, and this pair is what says whether moving it costs anything.
|
||||||
|
//
|
||||||
|
// The shape forces exactly that, every iteration: one CONSTANT vertex source (hits L1c after
|
||||||
|
// the first iteration) linked against a FRESH fragment source each time (misses L1c, and
|
||||||
|
// makes the program key miss too). So:
|
||||||
|
//
|
||||||
|
// cache off - two parses at glCompileShader, then the link.
|
||||||
|
// cache on - one parse at glCompileShader (the fragment), one deferred parse inside the
|
||||||
|
// link (the vertex), then the link.
|
||||||
|
//
|
||||||
|
// The parse count is identical, so these two should land within noise of each other. If the
|
||||||
|
// On arm is materially SLOWER, L1c is charging for something - the per-compile key build and
|
||||||
|
// hash over the full preprocessed source, or the loss of the claim-CAS reuse - and that cost
|
||||||
|
// shows up here and nowhere else.
|
||||||
|
//
|
||||||
|
// The distinct fragment sources also churn both front-end levels through their FIFO caps,
|
||||||
|
// which is the eviction behaviour a real shaderpack load produces; over a long run the
|
||||||
|
// constant vertex entry is occasionally evicted by that churn and re-inserted, so the L1c
|
||||||
|
// hit rate reported below is high but not exactly 1.0 per iteration.
|
||||||
|
namespace {
|
||||||
|
String UniqueFragmentSource(const Uint64 serial, const int padLines) {
|
||||||
|
return SwizzleLikeFragment("", padLines) +
|
||||||
|
"\n// unique-" + std::to_string(serial) + "\n";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static void BM_DeferredParseLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
Uint64 serial = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_DeferredParseLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_DeferredParseLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
Uint64 serial = 0;
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(~0ull, static_cast<int>(state.range(0)))); // prime the vertex entry
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
||||||
|
// Expected shape: L1 all misses (every program is new), L1c one hit (vertex) and one miss
|
||||||
|
// (fragment) per iteration.
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
||||||
|
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_DeferredParseLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
|
||||||
|
# breakdown of one program build, which needs its own clock around sub-steps that share
|
||||||
|
# set-up, and a plain main() keeps the output a table this can be read straight out of.
|
||||||
|
add_executable(
|
||||||
|
TranspileProfile
|
||||||
|
TranspileProfile.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(TranspileProfile PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
TranspileProfile PRIVATE
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
EGLStateContext* GetState() {
|
EGLStateContext* GetState() {
|
||||||
if (!MG_State::pEGLContext) {
|
if (!MG_State::pEGLContext) {
|
||||||
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
|
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
|
||||||
}
|
}
|
||||||
return MG_State::pEGLContext.get();
|
return MG_State::pEGLContext.get();
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->DestroySurface(dpy, surface);
|
state->DestroySurface(dpy, surface);
|
||||||
return EGL_NO_SURFACE;
|
return EGL_NO_SURFACE;
|
||||||
}
|
}
|
||||||
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||||
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||||
state->SetError(EGL_BAD_SURFACE);
|
state->SetError(EGL_BAD_SURFACE);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
||||||
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (releaseCurrentRequest) {
|
if (releaseCurrentRequest) {
|
||||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||||
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
state->SetError(EGL_BAD_ACCESS);
|
state->SetError(EGL_BAD_ACCESS);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||||
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||||
dpy, draw, read, ctx);
|
dpy, draw, read, ctx);
|
||||||
state->SetError(EGL_BAD_ACCESS);
|
state->SetError(EGL_BAD_ACCESS);
|
||||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->DestroySurface(dpy, surface);
|
state->DestroySurface(dpy, surface);
|
||||||
return EGL_NO_SURFACE;
|
return EGL_NO_SURFACE;
|
||||||
}
|
}
|
||||||
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
width = std::max<EGLint>(width, 1);
|
width = std::max<EGLint>(width, 1);
|
||||||
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||||
void* proc = MG_Impl::GetProcAddress(name);
|
void* proc = MG_Impl::GetProcAddress(name);
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
MGLOG_W("Failed to get function: %s", name);
|
MGLOG_D("Failed to get function: %s", name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return (__eglMustCastToProperFunctionPointerType)proc;
|
return (__eglMustCastToProperFunctionPointerType)proc;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||||
|
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
NamedBufferData,
|
NamedBufferData,
|
||||||
NamedBufferSubData,
|
NamedBufferSubData,
|
||||||
CopyNamedBufferSubData,
|
CopyNamedBufferSubData,
|
||||||
|
ClearBufferData,
|
||||||
|
ClearBufferSubData,
|
||||||
ClearNamedBufferData,
|
ClearNamedBufferData,
|
||||||
ClearNamedBufferSubData,
|
ClearNamedBufferSubData,
|
||||||
MapBufferRange,
|
MapBufferRange,
|
||||||
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return "NamedBufferSubData";
|
return "NamedBufferSubData";
|
||||||
case BufferOp::CopyNamedBufferSubData:
|
case BufferOp::CopyNamedBufferSubData:
|
||||||
return "CopyNamedBufferSubData";
|
return "CopyNamedBufferSubData";
|
||||||
|
case BufferOp::ClearBufferData:
|
||||||
|
return "ClearBufferData";
|
||||||
|
case BufferOp::ClearBufferSubData:
|
||||||
|
return "ClearBufferSubData";
|
||||||
case BufferOp::ClearNamedBufferData:
|
case BufferOp::ClearNamedBufferData:
|
||||||
return "ClearNamedBufferData";
|
return "ClearNamedBufferData";
|
||||||
case BufferOp::ClearNamedBufferSubData:
|
case BufferOp::ClearNamedBufferSubData:
|
||||||
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pattern is replicated verbatim, which is only the whole story while the client
|
|
||||||
// layout already matches the internal format - the case every entry point in practice
|
|
||||||
// uses, and the only one the conversion machinery here can express. Say so rather than
|
|
||||||
// quietly writing a differently-sized pattern.
|
|
||||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
|
||||||
if (sourceSize != elementSize) {
|
|
||||||
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
|
||||||
"converting between them is not implemented",
|
|
||||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
|
||||||
}
|
|
||||||
return elementSize;
|
return elementSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
|
||||||
|
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
|
||||||
|
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
|
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||||
|
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||||
|
|
||||||
|
Vector<Uint8> zeroInput;
|
||||||
|
const void* inputPixel = data;
|
||||||
|
if (inputPixel == nullptr) {
|
||||||
|
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
|
||||||
|
if (inputSize == 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
"format and type do not describe a source pixel."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
zeroInput.resize(inputSize);
|
||||||
|
inputPixel = zeroInput.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
|
||||||
|
internal, inputFormat, inputType, inputPixel, pattern)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
|
||||||
|
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
|
||||||
|
internalformat)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == nullptr) {
|
||||||
|
// GL defines a null clear value as all zero bits in the destination store, while
|
||||||
|
// retaining the format/type validation above.
|
||||||
|
pattern.assign(patternSize, 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||||
|
GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||||
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
||||||
if (patternSize == 0) return;
|
if (patternSize == 0) return;
|
||||||
|
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, op);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
||||||
if (size == 0) return;
|
if (size == 0) return;
|
||||||
|
|
||||||
Vector<Uint8> clearData(static_cast<SizeT>(size));
|
Vector<Uint8> pattern;
|
||||||
if (data) {
|
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
|
||||||
const auto* pattern = static_cast<const Uint8*>(data);
|
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
|
||||||
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
|
static_cast<SizeT>(size));
|
||||||
Memcpy(clearData.data() + at, pattern, patternSize);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Memset(clearData.data(), 0, clearData.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& GetBufferBindingSlot(BufferTarget target) {
|
auto& GetBufferBindingSlot(BufferTarget target) {
|
||||||
@@ -1197,16 +1226,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||||
|
type, data, BufferOp::ClearBufferData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
|
GLenum format, GLenum type, const void* data) {
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||||
|
BufferOp::ClearBufferSubData);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
||||||
if (!bufferObject) return;
|
if (!bufferObject) return;
|
||||||
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||||
type, data, BufferOp::ClearNamedBufferData);
|
type, data, BufferOp::ClearNamedBufferData);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
GLenum format, GLenum type, const void* data) {
|
GLenum format, GLenum type, const void* data) {
|
||||||
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||||
BufferOp::ClearNamedBufferSubData);
|
BufferOp::ClearNamedBufferSubData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1662,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
|
ClearBufferData_State(target, internalformat, format, type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
|
GLenum type, const void* data) {
|
||||||
|
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||||
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
||||||
GLsizeiptr size);
|
GLsizeiptr size);
|
||||||
|
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||||
|
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
|
GLenum type, const void* data);
|
||||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||||
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
GLenum type, const void* data);
|
GLenum type, const void* data);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||||
Bool ValidateBufferTarget(BufferTarget target) {
|
Bool ValidateBufferTarget(BufferTarget target) {
|
||||||
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
pointCount = std::min<SizeT>(pointCount, 4);
|
||||||
}
|
}
|
||||||
|
if (target == BufferTarget::AtomicCounter) {
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
|
||||||
|
// size: a counter buffer reaches a shader only as a lowered storage block, so the
|
||||||
|
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
|
||||||
|
pointCount = std::min<SizeT>(
|
||||||
|
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||||
|
}
|
||||||
return pointCount;
|
return pointCount;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||||
|
|
||||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
// Of the EXECUTABLE, not the live attach list: attaching a compute shader to an
|
||||||
|
// already-linked graphics program does not give that program a compute stage to
|
||||||
|
// dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the
|
||||||
|
// attach hands the backend a program whose SPIR-V has no compute module in it.
|
||||||
|
if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
@@ -108,6 +112,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||||
if (program != nullptr) {
|
if (program != nullptr) {
|
||||||
|
// A geometry stage writes what it emits, not what the draw assembled, and the
|
||||||
|
// amplification factor lives in the shader. Record that this span contained such
|
||||||
|
// a draw so the transform feedback queries keep their backend result for it.
|
||||||
|
if (program->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
||||||
|
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
|
||||||
|
}
|
||||||
// Capacity in captured vertices = the tightest bound buffer.
|
// Capacity in captured vertices = the tightest bound buffer.
|
||||||
Uint64 capacityVertices = ~0ull;
|
Uint64 capacityVertices = ~0ull;
|
||||||
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
||||||
@@ -127,6 +137,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
||||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||||
|
// Only draws that get this far are in the written counter at all. The instanced and
|
||||||
|
// indirect entry points never call this function, so a span that contains one is NOT
|
||||||
|
// fully accounted, and the queries must be able to tell: they compare this counter's
|
||||||
|
// delta against zero before standing in for the backend's own result.
|
||||||
|
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
||||||
@@ -151,14 +166,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
// The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the
|
||||||
if (!IsAcceptedPrimitiveMode(mode)) {
|
// state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM
|
||||||
|
// unconditionally, while "no current program" is not even a spec-listed draw error - it is
|
||||||
|
// MobileGL's own null-dereference guard - so it must never shadow the enum check
|
||||||
|
// (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode
|
||||||
|
// GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM).
|
||||||
|
static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) {
|
||||||
|
if (IsAcceptedPrimitiveMode(mode)) return true;
|
||||||
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(functionName, mode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -176,13 +203,58 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
|
|
||||||
|
// GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and
|
||||||
|
// GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and
|
||||||
|
// neither was implemented - which is two of the four sites
|
||||||
|
// KHR-GL43.transform_feedback.api_errors_test checks with one shared message string.
|
||||||
|
// The EVALUATION stage is what decides: a control stage cannot run without one, and a
|
||||||
|
// program carrying only an evaluation stage still tessellates, through GL's
|
||||||
|
// fixed-function pass-through control stage (11.2.2).
|
||||||
|
// Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a
|
||||||
|
// tessellation evaluation shader to an already-linked program does not put it in the
|
||||||
|
// executable, so reading the live list here would reject every non-GL_PATCHES draw
|
||||||
|
// against a program that does not tessellate - and keep rejecting them, since a detach
|
||||||
|
// is likewise deferred to the next link.
|
||||||
|
const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
||||||
|
if (tessellationActive && mode != GL_PATCHES) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", functionName,
|
||||||
|
"A program with a tessellation evaluation shader can only be drawn with GL_PATCHES."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!tessellationActive && mode == GL_PATCHES) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"GL_PATCHES requires an active tessellation evaluation shader."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// A geometry stage only accepts the primitive types that decompose into its declared
|
// A geometry stage only accepts the primitive types that decompose into its declared
|
||||||
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||||
// is the tessellation pipeline's input and reaches the geometry stage already
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
// converted, so it is not constrained here.
|
// converted, so it is not constrained here.
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
//
|
||||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
// "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
|
||||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
// primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
|
||||||
|
// is indistinguishable from no geometry shader by its reflected input type alone. The
|
||||||
|
// sentinel test this replaces therefore skipped the whole rule for exactly the geometry
|
||||||
|
// shaders whose input is the most restrictive one - every mode but GL_POINTS was
|
||||||
|
// accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
|
||||||
|
// program with GL_LINES and requires INVALID_OPERATION).
|
||||||
|
//
|
||||||
|
// And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so
|
||||||
|
// pairing it with the live attach list would re-point the very same 0-aliasing rather
|
||||||
|
// than remove it. In the window after glAttachShader(GS) on a linked program the live
|
||||||
|
// list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and
|
||||||
|
// the switch below would silently reject every mode but GL_POINTS.
|
||||||
|
const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry);
|
||||||
|
const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
|
if (geometryActive && mode != GL_PATCHES) {
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
switch (gsInput) {
|
switch (gsInput) {
|
||||||
case GL_POINTS:
|
case GL_POINTS:
|
||||||
@@ -216,13 +288,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// While transform feedback is active the draw's primitive type must match
|
// While transform feedback is active the draw's primitive type must match
|
||||||
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
||||||
// the constraint moves to the shader's output primitive type instead, so
|
// the constraint moves to the shader's output primitive type instead, so
|
||||||
// the draw mode itself is unconstrained here. A paused span is exempt: it
|
// the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
|
||||||
// captures nothing, so there is nothing for the mode to be incompatible with
|
// stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
|
||||||
// (GL 4.6 core 13.2.3).
|
// what is captured is the tessellator's output primitive, and the draw mode
|
||||||
|
// can only ever be GL_PATCHES. A paused span is exempt: it captures nothing,
|
||||||
|
// so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3).
|
||||||
|
const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||||
|
// Both stage tests are asked of the last link, for the same reason as the two guards
|
||||||
|
// above: what relocates the constraint is a stage the program actually RUNS, and an
|
||||||
|
// attach that has not been linked in yet gives it none.
|
||||||
|
const Bool feedbackModeIsProgramDriven =
|
||||||
|
feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
||||||
|
feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval));
|
||||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
|
!MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
|
||||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
|
||||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
|
||||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
switch (feedbackMode) {
|
switch (feedbackMode) {
|
||||||
@@ -303,10 +382,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
||||||
|
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
||||||
|
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
||||||
|
// everything a real driver would still do inside the block - argument validation and the
|
||||||
|
// errors it raises - happens exactly as it does outside one, and only the command itself is
|
||||||
|
// dropped. It is deliberately not on the frontend's transform-feedback accounting either:
|
||||||
|
// that mirrors what the capture stage would have written, and a conditional block around a
|
||||||
|
// capturing draw has no test coverage in either direction.
|
||||||
|
static Bool ConditionalRenderDiscardsCommand() {
|
||||||
|
return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
|
||||||
|
}
|
||||||
|
|
||||||
void Clear_Backend(GLbitfield mask) {
|
void Clear_Backend(GLbitfield mask) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +406,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +415,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +424,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -338,6 +433,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +441,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +459,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +467,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +476,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||||
maxdrawcount, stride);
|
maxdrawcount, stride);
|
||||||
}
|
}
|
||||||
@@ -385,6 +486,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||||
stride);
|
stride);
|
||||||
}
|
}
|
||||||
@@ -394,6 +496,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -403,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +516,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -421,6 +526,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -430,6 +536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||||
instancecount, baseinstance);
|
instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -439,6 +546,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +554,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||||
}
|
}
|
||||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
@@ -453,6 +562,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||||
baseinstance);
|
baseinstance);
|
||||||
}
|
}
|
||||||
@@ -461,6 +571,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +579,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +608,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
|
||||||
|
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +662,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,12 +712,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||||
@@ -715,12 +833,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
const void* indices, GLint basevertex) {
|
const void* indices, GLint basevertex) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
||||||
@@ -728,6 +848,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||||
@@ -736,6 +857,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex) {
|
GLsizei instancecount, GLint basevertex) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||||
@@ -743,18 +865,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLuint baseinstance) {
|
GLsizei instancecount, GLuint baseinstance) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||||
@@ -764,18 +889,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
GLuint baseinstance) {
|
GLuint baseinstance) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||||
@@ -783,6 +911,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
@@ -790,6 +919,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
@@ -797,6 +927,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (drawcount < 0) {
|
if (drawcount < 0) {
|
||||||
@@ -810,6 +941,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||||
GLsizei drawcount) {
|
GLsizei drawcount) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||||
@@ -817,6 +949,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||||
GLsizei drawcount, const GLint* basevertex) {
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||||
@@ -827,6 +960,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||||
|
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
@@ -1259,7 +1393,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
// "id is not the name of a transform feedback object" has to mean the same thing here
|
||||||
|
// as it does to glIsTransformFeedback, and the two predicates are not interchangeable:
|
||||||
|
// a name glGenTransformFeedbacks handed out is only reserved until it is first bound,
|
||||||
|
// and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName
|
||||||
|
// answers the reservation question - the right one for glBindTransformFeedback, which is
|
||||||
|
// what turns a reserved name into an object - so using it here let a generated-but-unbound
|
||||||
|
// name through to the completed-span check below and raised INVALID_OPERATION where the
|
||||||
|
// spec asks for INVALID_VALUE. Name 0 is the default object and always drawable.
|
||||||
|
if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||||
|
|
||||||
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||||
return (type)1; \
|
return (type)1; \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||||
@@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender)
|
DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
||||||
@@ -969,24 +969,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
||||||
@@ -1061,7 +1061,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, G
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||||
@@ -1849,7 +1849,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture,
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||||
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||||
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
|
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
@@ -547,7 +548,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||||
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
||||||
if (!blitNamedFramebuffer) {
|
if (!blitNamedFramebuffer) {
|
||||||
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||||
@@ -558,7 +559,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
||||||
if (!clearNamedFramebufferfv) {
|
if (!clearNamedFramebufferfv) {
|
||||||
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -568,7 +569,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
||||||
if (!clearNamedFramebufferfi) {
|
if (!clearNamedFramebufferfi) {
|
||||||
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||||
@@ -578,7 +579,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
||||||
if (!clearNamedFramebufferiv) {
|
if (!clearNamedFramebufferiv) {
|
||||||
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -588,7 +589,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
||||||
if (!clearNamedFramebufferuiv) {
|
if (!clearNamedFramebufferuiv) {
|
||||||
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -617,16 +618,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
return std::numeric_limits<Int>::max();
|
return std::numeric_limits<Int>::max();
|
||||||
}
|
}
|
||||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
return GetAdvertisedMaxSamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
|
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
|
||||||
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||||
// The multisample TEXTURE path already resolves the limit per format
|
// The multisample TEXTURE path resolves the limit per format the same way
|
||||||
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
|
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
|
||||||
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
|
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
|
||||||
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
|
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
|
||||||
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
|
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
|
||||||
|
// count it actually hands the driver.
|
||||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
return std::numeric_limits<Int>::max();
|
return std::numeric_limits<Int>::max();
|
||||||
@@ -645,7 +647,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!isIntegerFormat) {
|
if (!isIntegerFormat) {
|
||||||
return GetMaxRenderbufferSamples_State();
|
return GetMaxRenderbufferSamples_State();
|
||||||
}
|
}
|
||||||
return std::max(dynamicParameters.MaxIntegerSamples, 1);
|
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
|
||||||
|
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
|
||||||
|
// clamps to it, while the frontend honours what it advertised.
|
||||||
|
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||||
@@ -2608,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3148,15 +3161,55 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
|
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The three argument errors GL 4.6 core 18.3.1 asks a blit for. They have to be raised here,
|
||||||
|
// in the backend-independent frontend: DirectGLES drains the driver's error queue around the
|
||||||
|
// blit on purpose (that is how the resolve fallback probes the driver), so an ES-side
|
||||||
|
// rejection never reaches the application and glGetError() answered GL_NO_ERROR for a call
|
||||||
|
// the spec requires to fail (KHR-GL30.api.coverage's glBlitFramebuffer sub-check). DirectVulkan
|
||||||
|
// already dropped the bad-filter and LINEAR-with-depth/stencil calls on the floor with a log
|
||||||
|
// line (VulkanRenderer::BlitFramebuffer), so the only thing that changes for it is that the
|
||||||
|
// error is now visible where the spec says it should be.
|
||||||
|
static Bool ValidateBlitMaskAndFilter(const char* functionName, GLbitfield mask, GLenum filter) {
|
||||||
|
constexpr GLbitfield kBlitMaskBits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||||
|
if ((mask & ~kBlitMaskBits) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"mask contains bits other than GL_COLOR_BUFFER_BIT, "
|
||||||
|
"GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filter != GL_NEAREST && filter != GL_LINEAR) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"filter must be GL_NEAREST or GL_LINEAR."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Depth and stencil have no meaningful interpolation, so GL_LINEAR is rejected outright
|
||||||
|
// rather than downgraded - even when the mask also carries the colour bit.
|
||||||
|
if (filter == GL_LINEAR && (mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"GL_LINEAR filtering is not allowed when mask includes "
|
||||||
|
"GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
|
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
|
||||||
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
|
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
|
||||||
GLenum filter) {
|
GLenum filter) {
|
||||||
|
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
|
||||||
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||||
dstY1, mask, filter);
|
dstY1, mask, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||||
|
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
|
||||||
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||||
|
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
@@ -24,9 +25,15 @@
|
|||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||||
|
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||||
|
// this file's blend-state queries name unqualified.
|
||||||
|
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
enum class IndexedBufferQueryKind {
|
enum class IndexedBufferQueryKind {
|
||||||
Binding,
|
Binding,
|
||||||
@@ -40,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr GLint kFrontendMaxComputeUniformComponents = 1024;
|
// Shared with the glslang resource table for the same reason as the atomic-counter
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounters = 8;
|
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8;
|
constexpr GLint kFrontendMaxComputeUniformComponents =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
|
||||||
|
// Every atomic-counter limit is shared with the glslang resource table
|
||||||
|
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
|
||||||
|
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
|
||||||
|
// used to be independent tables that disagreed on both the binding count and the buffer
|
||||||
|
// size. Never move one of these without the other.
|
||||||
|
constexpr GLint kFrontendMaxComputeAtomicCounters =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
||||||
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
||||||
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8;
|
constexpr GLint kFrontendMaxCombinedAtomicCounters =
|
||||||
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8;
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxFragmentAtomicCounters =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||||
@@ -60,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||||
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
|
||||||
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
|
||||||
|
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
|
||||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||||
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
||||||
// limits they advertise still have to be legal.
|
// limits they advertise still have to be legal.
|
||||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||||
@@ -97,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendSubpixelBits = 4;
|
constexpr GLint kFrontendSubpixelBits = 4;
|
||||||
constexpr GLint kFrontendMaxSamples = 4;
|
constexpr GLint kFrontendMaxSamples = 4;
|
||||||
|
|
||||||
|
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
||||||
|
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
||||||
|
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
|
||||||
|
// shader is allowed to compare the built-in constant against this query.
|
||||||
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
||||||
return index < 3 ? 65535 : 0;
|
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
||||||
return index < 2 ? 1024 : (index == 2 ? 64 : 0);
|
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||||
@@ -180,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||||
}
|
}
|
||||||
|
if (bufferTarget == BufferTarget::AtomicCounter) {
|
||||||
|
// The counter family's binding count is NOT the state layer's array size: a
|
||||||
|
// counter buffer only reaches a shader as a lowered storage block, so what an
|
||||||
|
// implementation can serve is the reserved range, and that number is also what
|
||||||
|
// glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
|
||||||
|
// glBindBufferBase's all report the same ceiling.
|
||||||
|
return std::min(frontendCount,
|
||||||
|
static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||||
|
}
|
||||||
return frontendCount;
|
return frontendCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +245,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
|
||||||
|
// constant to be "restored" - these used to return a flat 16 for vertex, geometry and
|
||||||
|
// both tessellation stages, which is wrong on any host that does not serve storage
|
||||||
|
// blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
|
||||||
|
// 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
|
||||||
|
// why the conformance suite gates each such test on the query instead of assuming it.
|
||||||
|
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
|
||||||
|
// bought nothing: the program still failed to link inside the backend, the frontend
|
||||||
|
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
|
||||||
|
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
|
||||||
|
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
|
||||||
|
const MG_Backend::DynamicBackendParameters& parameters =
|
||||||
|
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||||
|
: kBackendlessDefaults;
|
||||||
|
return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
|
||||||
|
}
|
||||||
|
|
||||||
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
||||||
if (pname == GL_DRAW_BUFFER) {
|
if (pname == GL_DRAW_BUFFER) {
|
||||||
drawBufferIndex = 0;
|
drawBufferIndex = 0;
|
||||||
@@ -339,26 +394,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||||
// scissor box and one depth range, so every in-range index answers with that single
|
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||||
// has no case for these, so routing them through it returned zeros.
|
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||||
|
// index with viewport 0's value, which is what
|
||||||
|
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||||
Bool IsIndexedViewportQuery(GLenum target) {
|
Bool IsIndexedViewportQuery(GLenum target) {
|
||||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||||
|
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||||
|
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||||
|
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||||
|
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||||
|
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||||
|
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||||
GLint maxViewports = 0;
|
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
|
||||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||||
|
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||||
|
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||||
|
// getter width funnels through this so they can never disagree with each other.
|
||||||
|
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||||
|
switch (target) {
|
||||||
|
case GL_VIEWPORT: {
|
||||||
|
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||||
|
out[0] = viewport.x();
|
||||||
|
out[1] = viewport.y();
|
||||||
|
out[2] = viewport.z();
|
||||||
|
out[3] = viewport.w();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_SCISSOR_BOX: {
|
||||||
|
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||||
|
out[0] = static_cast<GLfloat>(box.x());
|
||||||
|
out[1] = static_cast<GLfloat>(box.y());
|
||||||
|
out[2] = static_cast<GLfloat>(box.z());
|
||||||
|
out[3] = static_cast<GLfloat>(box.w());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
|
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||||
|
out[0] = range.x();
|
||||||
|
out[1] = range.y();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||||
|
static_cast<Uint32>(target));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||||
for (SizeT i = 0; i < count; ++i) {
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||||
@@ -372,6 +471,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
||||||
|
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
||||||
|
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
||||||
|
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
||||||
|
// just told the application to use. The backends clamp the realised count instead.
|
||||||
|
GLint GetAdvertisedMaxSamples() {
|
||||||
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
|
return kFrontendMaxSamples;
|
||||||
|
}
|
||||||
|
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
|
||||||
|
}
|
||||||
|
|
||||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||||
const GLubyte* GetString(GLenum name) {
|
const GLubyte* GetString(GLenum name) {
|
||||||
static String vendorString;
|
static String vendorString;
|
||||||
@@ -383,7 +494,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return (GLubyte*)"Unknown";
|
return (GLubyte*)"Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,7 +553,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return (GLubyte*)"Unknown";
|
return (GLubyte*)"Unknown";
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
@@ -629,6 +740,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||||
|
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||||
|
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||||
|
case GL_VIEWPORT: {
|
||||||
|
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||||
|
params[0] = viewport.x();
|
||||||
|
params[1] = viewport.y();
|
||||||
|
params[2] = viewport.z();
|
||||||
|
params[3] = viewport.w();
|
||||||
|
return;
|
||||||
|
}
|
||||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||||
@@ -792,15 +914,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||||
|
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||||
|
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||||
|
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||||
|
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||||
|
*data = IsEnabledi(target, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||||
// viewport and one scissor box, so every in-range index reports that one.
|
|
||||||
case GL_VIEWPORT:
|
case GL_VIEWPORT:
|
||||||
case GL_SCISSOR_BOX:
|
case GL_SCISSOR_BOX:
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
GetIntegerv(target, data);
|
GLfloat values[4] = {};
|
||||||
|
ReadIndexedViewportStateFloat(target, index, values);
|
||||||
|
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||||
|
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||||
|
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||||
case GL_VERTEX_BINDING_BUFFER:
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
@@ -927,7 +1066,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
GetFloatv(target, data);
|
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||||
|
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||||
|
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||||
|
ReadIndexedViewportStateFloat(target, index, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -944,7 +1086,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
GetDoublev(target, data);
|
GLfloat values[4] = {};
|
||||||
|
ReadIndexedViewportStateFloat(target, index, values);
|
||||||
|
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
data[i] = static_cast<GLdouble>(values[i]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -1020,7 +1167,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// frontend-only value simply is not in the driver's table.
|
// frontend-only value simply is not in the driver's table.
|
||||||
GLint values[4] = {};
|
GLint values[4] = {};
|
||||||
GetIntegeri_v(target, index, values);
|
GetIntegeri_v(target, index, values);
|
||||||
*data = static_cast<GLint64>(values[0]);
|
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||||
|
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||||
|
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
data[i] = static_cast<GLint64>(values[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||||
@@ -1420,15 +1572,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_LINE_WIDTH:
|
case GL_LINE_WIDTH:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
||||||
return;
|
return;
|
||||||
case GL_LAYER_PROVOKING_VERTEX:
|
|
||||||
*params = GL_LAST_VERTEX_CONVENTION;
|
|
||||||
return;
|
|
||||||
case GL_LOGIC_OP_MODE:
|
case GL_LOGIC_OP_MODE:
|
||||||
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
||||||
return;
|
return;
|
||||||
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxCombinedAtomicCounters;
|
*params = kFrontendMaxCombinedAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxCombinedAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||||
return;
|
return;
|
||||||
@@ -1444,8 +1596,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxFragmentAtomicCounters;
|
*params = kFrontendMaxFragmentAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxFragmentAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||||
*params = ClampStorageBlockCount(16); // TODO
|
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxFragmentInputComponents;
|
*params = kFrontendMaxFragmentInputComponents;
|
||||||
@@ -1471,7 +1626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||||
*params = ClampStorageBlockCount(16); // TODO
|
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxGeometryInputComponents;
|
*params = kFrontendMaxGeometryInputComponents;
|
||||||
@@ -1506,7 +1661,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
||||||
*params = 64; // TODO
|
// The same constant the map paths align to (MG_State/GLState/BufferState/
|
||||||
|
// PipeResource.h), never a literal: this number is a PROMISE about the pointers
|
||||||
|
// glMapBuffer and glMapBufferRange return, and the two used to be unrelated - the
|
||||||
|
// query said 64 while the pointers came out of a std::vector aligned to 16.
|
||||||
|
*params = static_cast<GLint>(MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_LABEL_LENGTH:
|
case GL_MAX_LABEL_LENGTH:
|
||||||
*params = 256; // TODO
|
*params = 256; // TODO
|
||||||
@@ -1542,16 +1701,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = 0;
|
*params = 0;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||||
*params = ClampStorageBlockCount(16); // TODO
|
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||||
*params = ClampStorageBlockCount(16); // TODO
|
*params =
|
||||||
|
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||||
*params = 15; // TODO
|
*params = 15; // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_UNIFORM_LOCATIONS:
|
case GL_MAX_UNIFORM_LOCATIONS:
|
||||||
*params = 1024 * 4; // TODO
|
// The same constant the link's location allocator enforces - see ProgramObject.
|
||||||
|
*params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VARYING_COMPONENTS:
|
case GL_MAX_VARYING_COMPONENTS:
|
||||||
*params = kFrontendMaxVaryingComponents;
|
*params = kFrontendMaxVaryingComponents;
|
||||||
@@ -1571,7 +1732,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
*params = ClampStorageBlockCount(16); // TODO
|
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||||
*params = kFrontendMaxVertexUniformComponents;
|
*params = kFrontendMaxVertexUniformComponents;
|
||||||
@@ -1881,6 +2042,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_BUFFER_START:
|
case GL_UNIFORM_BUFFER_START:
|
||||||
RecordIndexedOnlyGetterError(__func__, pname);
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
return;
|
return;
|
||||||
|
// glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
|
||||||
|
// is the one indexed-buffer family whose non-indexed query was never answered - so it
|
||||||
|
// fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
|
||||||
|
// stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
||||||
|
if (const auto& obj =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
|
||||||
|
*params = static_cast<GLint>(obj->GetExternalIndex());
|
||||||
|
} else {
|
||||||
|
*params = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_START:
|
||||||
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
|
return;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
|
return;
|
||||||
case GL_UNPACK_ALIGNMENT:
|
case GL_UNPACK_ALIGNMENT:
|
||||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
||||||
return;
|
return;
|
||||||
@@ -1935,9 +2114,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[3] = vp.w();
|
params[3] = vp.w();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
|
||||||
*params = GL_LAST_VERTEX_CONVENTION;
|
|
||||||
return;
|
|
||||||
case GL_MAX_ELEMENT_INDEX:
|
case GL_MAX_ELEMENT_INDEX:
|
||||||
*params = 1024 * 1024; // TODO
|
*params = 1024 * 1024; // TODO
|
||||||
return;
|
return;
|
||||||
@@ -1954,7 +2130,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
@@ -2025,8 +2201,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_CLIP_DISTANCES:
|
case GL_MAX_CLIP_DISTANCES:
|
||||||
*params = dynamicParameters.MaxClipDistances;
|
*params = dynamicParameters.MaxClipDistances;
|
||||||
break;
|
break;
|
||||||
|
// Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
|
||||||
|
// 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
|
||||||
|
// wherever they do not actually pin a convention - claiming one is a statement about
|
||||||
|
// which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
|
||||||
|
// rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
|
||||||
|
// DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
|
||||||
|
// accepts all four values, and .provoking_vertex - which failed on both devices, in
|
||||||
|
// OPPOSITE directions - stops verifying as soon as either answer is undefined.
|
||||||
|
case GL_LAYER_PROVOKING_VERTEX:
|
||||||
|
*params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
|
||||||
|
break;
|
||||||
|
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
||||||
|
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
||||||
|
break;
|
||||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||||
*params = dynamicParameters.MaxColorTextureSamples;
|
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||||
@@ -2056,7 +2246,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||||
*params = dynamicParameters.MaxDepthTextureSamples;
|
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
||||||
break;
|
break;
|
||||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||||
*params = dynamicParameters.MaxFramebufferWidth;
|
*params = dynamicParameters.MaxFramebufferWidth;
|
||||||
@@ -2083,7 +2273,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_INTEGER_SAMPLES:
|
case GL_MAX_INTEGER_SAMPLES:
|
||||||
*params = dynamicParameters.MaxIntegerSamples;
|
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||||
break;
|
break;
|
||||||
case GL_MAX_RENDERBUFFER_SIZE:
|
case GL_MAX_RENDERBUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxRenderbufferSize;
|
*params = dynamicParameters.MaxRenderbufferSize;
|
||||||
@@ -2116,18 +2306,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static_cast<Uint64>(INT32_MAX)));
|
static_cast<Uint64>(INT32_MAX)));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||||
|
// NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
|
||||||
|
// clamps this family to the range a lowered counter block can actually be served
|
||||||
|
// from, which is the same number glslang compiles a layout(binding = N) atomic_uint
|
||||||
|
// against and the same one glBindBufferBase validates an index against.
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
// The conformance suite splits this evenly across every advertised binding point and
|
// The conformance suite splits this evenly across every advertised binding point and
|
||||||
// binds all of them in one glBindBuffersRange
|
// binds all of them in one glBindBuffersRange
|
||||||
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
|
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
|
||||||
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
|
// a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
|
||||||
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
// shared constant is 16384 over 8 binding points, which divides.
|
||||||
// one counter per binding point.
|
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||||
*params = std::max<GLint>(
|
|
||||||
kFrontendMaxAtomicCounterBufferSize,
|
|
||||||
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
@@ -2192,7 +2383,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.MaxViewportHeight;
|
params[1] = dynamicParameters.MaxViewportHeight;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_VIEWPORTS:
|
case GL_MAX_VIEWPORTS:
|
||||||
*params = dynamicParameters.MaxViewports;
|
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||||
|
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||||
|
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||||
|
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||||
|
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||||
|
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||||
|
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||||
|
// carrying that device number for exactly that decision.
|
||||||
|
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||||
break;
|
break;
|
||||||
case GL_MINOR_VERSION:
|
case GL_MINOR_VERSION:
|
||||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||||
@@ -2241,14 +2440,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: dynamicParameters.MaxDrawBuffers;
|
: dynamicParameters.MaxDrawBuffers;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_SAMPLES:
|
case GL_MAX_SAMPLES:
|
||||||
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
|
*params = GetAdvertisedMaxSamples();
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
||||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
||||||
std::format("Invalid enum: 0x{:X}", pname)));
|
std::format("Invalid enum: 0x{:X}", pname)));
|
||||||
|
|||||||
@@ -24,4 +24,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
GLenum GetError();
|
GLenum GetError();
|
||||||
GLenum GetGraphicsResetStatus();
|
GLenum GetGraphicsResetStatus();
|
||||||
|
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
|
||||||
|
// core minimum. Frontend multisample validators have to honour this ceiling for every
|
||||||
|
// format, otherwise MobileGL rejects a sample count it advertised itself.
|
||||||
|
GLint GetAdvertisedMaxSamples();
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
||||||
|
// pointing into the TProgram's pool allocator. See ProgramObject::TypeFacts.
|
||||||
|
using TypeFactsRef = const MG_State::GLState::ProgramObject::TypeFacts&;
|
||||||
static GLint BoolToGLInt(bool value) {
|
static GLint BoolToGLInt(bool value) {
|
||||||
return value ? GL_TRUE : GL_FALSE;
|
return value ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -223,14 +226,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetOpaqueUniformUnitLimit(const glslang::TType* type) {
|
GLint GetOpaqueUniformUnitLimit(const TypeFactsRef type) {
|
||||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||||
if (type && type->isImage()) return dynamicParameters.MaxImageUnits;
|
if (type.isImage) return dynamicParameters.MaxImageUnits;
|
||||||
if (type && type->isTexture()) return dynamicParameters.MaxCombinedTextureImageUnits;
|
if (type.isTexture) return dynamicParameters.MaxCombinedTextureImageUnits;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ValidateOpaqueUniformUnit(const char* functionName, const glslang::TType* type, GLint unit) {
|
bool ValidateOpaqueUniformUnit(const char* functionName, const TypeFactsRef type, GLint unit) {
|
||||||
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
||||||
if (unit < 0 || unit >= limit) {
|
if (unit < 0 || unit >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -525,6 +528,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_ARRAY_STRIDE:
|
case GL_UNIFORM_ARRAY_STRIDE:
|
||||||
case GL_UNIFORM_MATRIX_STRIDE:
|
case GL_UNIFORM_MATRIX_STRIDE:
|
||||||
case GL_UNIFORM_IS_ROW_MAJOR:
|
case GL_UNIFORM_IS_ROW_MAJOR:
|
||||||
|
// GL 4.2 / ARB_shader_atomic_counters adds this one to the accepted set. Leaving it
|
||||||
|
// out did not merely lose the answer: the leftover GL_INVALID_ENUM is what made
|
||||||
|
// KHR-GL43.shader_atomic_counters.basic-program-query force a FAIL.
|
||||||
|
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -580,6 +587,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_IS_ROW_MAJOR:
|
case GL_UNIFORM_IS_ROW_MAJOR:
|
||||||
params[i] = programObject->GetActiveUniformIsRowMajor(idx);
|
params[i] = programObject->GetActiveUniformIsRowMajor(idx);
|
||||||
break;
|
break;
|
||||||
|
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
// Index into the GL_ACTIVE_ATOMIC_COUNTER_BUFFERS list, -1 for every uniform
|
||||||
|
// that is not an atomic counter (GL 4.6 core table 7.6).
|
||||||
|
params[i] = programObject->GetActiveUniformAtomicCounterBufferIndex(idx);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -642,7 +654,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
||||||
*params = programObject->GetActiveAtomicCounterCount();
|
// Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers
|
||||||
|
// neither: the relaxed parse has already turned every atomic_uint into a plain uint
|
||||||
|
// member of a synthesized storage block by the time it builds its reflection, so it
|
||||||
|
// reports zero. The interface-query model recovers the buffers from those blocks and
|
||||||
|
// is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)
|
||||||
|
// already answers - the two queries are required to agree.
|
||||||
|
*params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER);
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_ATTRIBUTES:
|
case GL_ACTIVE_ATTRIBUTES:
|
||||||
@@ -662,7 +680,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
|
case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
|
||||||
*params = programObject->GetActiveUniformBlocksCount();
|
// Uniform blocks only. GetActiveUniformBlocksCount() is the internal block space,
|
||||||
|
// which also carries the storage blocks and the synthesized atomic counter blocks.
|
||||||
|
*params = programObject->GetGlUniformBlockCount();
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
|
case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
|
||||||
@@ -682,7 +702,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
// "a linked program object with a compute shader" is one whose EXECUTABLE has the
|
||||||
|
// stage: the local size below is a link artifact, so an attached-but-not-yet-linked
|
||||||
|
// compute shader would answer this query with the previous link's (absent) value
|
||||||
|
// instead of the INVALID_OPERATION GL 4.6 core 7.13 asks for.
|
||||||
|
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Compute)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
@@ -856,10 +880,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||||
// each float back to the queried type, and it undoes the same padding itself.
|
// each float back to the queried type, and it undoes the same padding itself.
|
||||||
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
// Float matrices only, in both senses: a DOUBLE matrix never comes through here, whether its
|
||||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
// program was demoted (components are floats, the query is not) or kept its doubles (the
|
||||||
const Int columns = ttype->getMatrixCols();
|
// column stride is a dvec4's, and the caller's converting branch already walks it component
|
||||||
const Int rows = ttype->getMatrixRows();
|
// by component with the right one).
|
||||||
|
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
|
||||||
|
if (!ttype.isMatrix || ttype.isDouble) return false;
|
||||||
|
const Int columns = ttype.matrixCols;
|
||||||
|
const Int rows = ttype.matrixRows;
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
||||||
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
||||||
@@ -868,11 +896,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||||
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
// everything except a matrix, whose padded columns make it wider, and a `double` on a
|
||||||
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
// program whose modules were demoted, where it is half. The rule itself lives on
|
||||||
// one and two copies of a layout rule is one too many.
|
// ProgramObject, because the pipeline composite's uniform refresh needs the same one and
|
||||||
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
// two copies of a layout rule is one too many.
|
||||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize, const Bool nativeFloat64) {
|
||||||
|
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize, nativeFloat64);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||||
@@ -904,20 +933,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = (char*)programObject->MapUBO();
|
char* pUBO = (char*)programObject->MapUBO();
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
||||||
|
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
program, location);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||||
// which for a `double` uniform is twice its storage - every 64-bit float is
|
// which on a DEMOTED program is twice a `double` uniform's storage - its 64-bit
|
||||||
// narrowed before the module reaches a backend, so the slot holds floats. The
|
// floats were narrowed before the module reached a backend, so the slot holds
|
||||||
// typed entry points (glGetUniformdv and friends) go through
|
// floats. The typed entry points (glGetUniformdv and friends) go through
|
||||||
// GetUniformScalar_State, which converts component by component; this raw
|
// GetUniformScalar_State, which converts component by component; this raw
|
||||||
// copy has no type to convert with, so it is bounded rather than converted.
|
// copy has no type to convert with, so it is bounded rather than converted.
|
||||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||||
@@ -958,11 +988,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
||||||
|
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
program, location);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -971,28 +1002,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A double-precision uniform is the one case where the stored component type differs
|
// A double-precision uniform is the one case where the stored component type can differ
|
||||||
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
// from the DECLARED one for a non-opaque uniform: on a DEMOTED program the shader's
|
||||||
// narrowed to 32 bits before the module reaches a backend
|
// 64-bit floats were narrowed to 32 before the module reached the backend
|
||||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||||
// two components reinterpreted as one. Read component by component and let GL's
|
// two components reinterpreted as one. A program that KEPT its doubles stores real ones
|
||||||
|
// at the dvec4 column stride instead, so the width and the stride both move; everything
|
||||||
|
// else about this walk is the same. Read component by component either way and let GL's
|
||||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||||
// widens back to the queried type, having lost precision at the glUniform*d that
|
// widens back to the queried type, having lost precision - where it lost any - at the
|
||||||
// stored it and not here.
|
// glUniform*d that stored it and not here.
|
||||||
if (ttype->getBasicType() == glslang::EbtDouble) {
|
if (ttype.isDouble) {
|
||||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
|
||||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
const Int rows = ttype.isMatrix ? ttype.matrixRows
|
||||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
: (ttype.isVector ? ttype.vectorSize : 1);
|
||||||
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
// A non-matrix is one tightly packed run and never reaches the stride at all.
|
||||||
// tightly packed run and never reaches the stride at all.
|
const SizeT columnStride =
|
||||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
MG_State::GLState::ProgramObject::UniformMatrixColumnStride(ttype, nativeFloat64);
|
||||||
|
const SizeT componentSize = nativeFloat64 ? sizeof(GLdouble) : sizeof(GLfloat);
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
for (Int row = 0; row < rows; ++row) {
|
for (Int row = 0; row < rows; ++row) {
|
||||||
GLfloat component = 0.0f;
|
GLdouble component = 0.0;
|
||||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
if (nativeFloat64) {
|
||||||
sizeof(component));
|
Memcpy(&component, pUBO + offset + column * columnStride + row * componentSize,
|
||||||
|
sizeof(GLdouble));
|
||||||
|
} else {
|
||||||
|
GLfloat narrow = 0.0f;
|
||||||
|
Memcpy(&narrow, pUBO + offset + column * columnStride + row * componentSize,
|
||||||
|
sizeof(narrow));
|
||||||
|
component = static_cast<GLdouble>(narrow);
|
||||||
|
}
|
||||||
if constexpr (std::is_integral_v<T>) {
|
if constexpr (std::is_integral_v<T>) {
|
||||||
// Rounded to the nearest integer and clamped into the queried type's
|
// Rounded to the nearest integer and clamped into the queried type's
|
||||||
// range, so a negative double read through glGetUniformuiv is 0
|
// range, so a negative double read through glGetUniformuiv is 0
|
||||||
@@ -1057,21 +1098,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool allowVSOnlyPrograms;
|
// Read fresh every link, never latched in a static: the capability is
|
||||||
static Bool initialized = false;
|
// per-backend, and a latch would freeze it across a backend teardown +
|
||||||
if (!initialized) {
|
// re-initialization (the previous function-static memo here never even set
|
||||||
|
// its own initialized flag, so it re-read every call anyway - this makes
|
||||||
|
// the always-fresh behavior the stated one). A struct-field read per
|
||||||
|
// glLinkProgram costs nothing.
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const Bool allowVSOnlyPrograms =
|
||||||
allowVSOnlyPrograms = (Int)rendererInfo.StaticBackendCapability.AllowVSOnlyPrograms;
|
activeBackendObject->GetRendererInfo().StaticBackendCapability.AllowVSOnlyPrograms;
|
||||||
}
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
|
||||||
if (activeBackendObject) {
|
|
||||||
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
||||||
}
|
|
||||||
programObject->Link(!allowVSOnlyPrograms);
|
programObject->Link(!allowVSOnlyPrograms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1152,7 +1192,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
SizeT writeSize = ItemCount * sizeof(T);
|
SizeT writeSize = ItemCount * sizeof(T);
|
||||||
if (size < writeSize) {
|
if (size < writeSize) {
|
||||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||||
"bytes; clamping",
|
"bytes; clamping",
|
||||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||||
writeSize = size;
|
writeSize = size;
|
||||||
@@ -1173,7 +1213,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||||
// Should not happen: linking gives every settable uniform backing
|
// Should not happen: linking gives every settable uniform backing
|
||||||
// storage. Log and drop the write instead of faulting.
|
// storage. Log and drop the write instead of faulting.
|
||||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||||
"uboSize=%zu); dropping write",
|
"uboSize=%zu); dropping write",
|
||||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||||
writeSize, uboSize);
|
writeSize, uboSize);
|
||||||
@@ -1192,8 +1232,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||||
programObject.MarkUBOContentDirty();
|
programObject.MarkUBOContentDirty();
|
||||||
} else {
|
} else {
|
||||||
auto* ttype = programObject.GetUniformTType(location);
|
const auto& ttype = programObject.GetUniformTypeFacts(location);
|
||||||
if (!ttype->isTexture() && !ttype->isImage()) return;
|
if (!ttype.isTexture && !ttype.isImage) return;
|
||||||
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1264,17 +1304,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
// Whether the program a uniform write is about to land in stores 64-bit floats at their
|
||||||
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
// declared width. Answered off the PROGRAM, never off the live backend: it describes the
|
||||||
|
// modules that were actually built for it, and a backend with native fp64 still demotes a
|
||||||
|
// program whose vertex stage declares a Float64 input (see ProgramSpirvTask::GenerateSpirv).
|
||||||
|
// Nullptr - no current program, or a name that is not a program - answers false and lets the
|
||||||
|
// callee record the same error it always did.
|
||||||
|
Bool CurrentProgramUsesNativeFloat64() {
|
||||||
|
if (MG_State::pGLContext == nullptr) return false;
|
||||||
|
const auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||||
|
return programObject != nullptr && programObject->UsesNativeFloat64();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool NamedProgramUsesNativeFloat64(GLuint program) {
|
||||||
|
const auto& programObject = TryToGetProgramObject(program);
|
||||||
|
return programObject != nullptr && programObject->GetLinkStatus() && programObject->UsesNativeFloat64();
|
||||||
|
}
|
||||||
|
|
||||||
|
// glUniform*d / glUniformMatrix*dv. On a DEMOTED program neither needs a layout of its own:
|
||||||
|
// the transpile chain narrowed every 64-bit float in the shader to 32
|
||||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||||
// path is what keeps the two in step; a separate double-shaped layout here would write
|
// path is what keeps the two in step; a separate double-shaped layout there would write
|
||||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||||
//
|
//
|
||||||
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
// The narrowing is the same static_cast the demoted shader's own arithmetic performs, so the
|
||||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||||
|
//
|
||||||
|
// On a program that KEPT its doubles the reverse is true and for the same reason: its global
|
||||||
|
// UBO really does hold 8-byte components, so narrowing would leave a float bit pattern in the
|
||||||
|
// low half of a double slot - which is not a precision loss but a garbage value. The 64-bit
|
||||||
|
// values go through unchanged then, and the upload path is width-agnostic (it is templated on
|
||||||
|
// the component type and bounded by the uniform's own slot span).
|
||||||
|
//
|
||||||
|
// Note TryToGetProgramObject / GetProgramForUniform run TWICE on this path, once for the
|
||||||
|
// width question and once inside the call below. That is a lookup and a join on an entry
|
||||||
|
// point no shader pack uses; the alternative is duplicating both functions' whole validation
|
||||||
|
// sequence here, which is the thing that must not drift.
|
||||||
template <GLsizei ItemCount>
|
template <GLsizei ItemCount>
|
||||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
if (value == nullptr || count <= 0) {
|
if (value == nullptr || count <= 0) {
|
||||||
@@ -1283,6 +1351,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (location != -1 && CurrentProgramUsesNativeFloat64()) {
|
||||||
|
Uniformv_State<ItemCount>(location, count, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||||
@@ -1294,6 +1366,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (location != -1 && NamedProgramUsesNativeFloat64(program)) {
|
||||||
|
ProgramUniformv_State<ItemCount>(program, location, count, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||||
@@ -1345,15 +1421,63 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
// glUniformMatrix*dv / glProgramUniformMatrix*dv on a program that KEPT its doubles. Same
|
||||||
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
// walk as UniformMatrixfv_Object down to the last branch, and deliberately a copy of it
|
||||||
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
// rather than a template over the component type: the two differ in exactly one number that
|
||||||
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
// is not derivable from the component type alone - std140 pads a double matrix's column out
|
||||||
// is then the one implementation both spellings share.
|
// to a dvec4 (32 bytes) unless the column is a dvec2, which is already 16 - and folding that
|
||||||
|
// into the float version would put a per-call branch on the hot glUniformMatrix4fv path
|
||||||
|
// Minecraft calls thousands of times a frame for a case no shader pack ever takes.
|
||||||
|
template <typename Program>
|
||||||
|
void UniformMatrixdvNative_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value, Int columns, Int rows,
|
||||||
|
const String& ownerDescription) {
|
||||||
|
const SizeT columnStride = rows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
|
||||||
|
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||||
|
GLdouble column[4] = {};
|
||||||
|
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||||
|
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||||
|
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||||
|
RecordInvalidUniformLocationError("glUniformMatrixdv", location + matrix, ownerDescription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "glUniformMatrixdv",
|
||||||
|
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLdouble* source = value + static_cast<SizeT>(matrix) * componentCount;
|
||||||
|
for (Int c = 0; c < columns; ++c) {
|
||||||
|
for (Int r = 0; r < rows; ++r) {
|
||||||
|
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||||
|
}
|
||||||
|
const SizeT byteOffset = static_cast<SizeT>(c) * columnStride;
|
||||||
|
switch (rows) {
|
||||||
|
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glUniformMatrix*dv / glProgramUniformMatrix*dv. On a DEMOTED program this narrows to the
|
||||||
|
// float form and hands it straight over: after DemoteFloat64Pass a `dmat4` uniform is a
|
||||||
|
// `mat4` in the shader and a mat4-shaped slot in the global UBO, columns padded to a vec4
|
||||||
|
// and all. Everything else about the call - transpose handling, the array-element walk, the
|
||||||
|
// opaque-uniform refusal - is then the one implementation both spellings share. A program
|
||||||
|
// that kept its doubles gets the same walk at double width and the wider column stride.
|
||||||
template <typename Program>
|
template <typename Program>
|
||||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLdouble* value, Int columns, Int rows) {
|
const GLdouble* value, Int columns, Int rows) {
|
||||||
if (value == nullptr || count <= 0) return;
|
if (value == nullptr || count <= 0) return;
|
||||||
|
if (programObject.UsesNativeFloat64()) {
|
||||||
|
UniformMatrixdvNative_Object(programObject, location, count, transpose, value, columns, rows,
|
||||||
|
"the current program object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
@@ -1695,7 +1819,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return GL_INVALID_INDEX;
|
return GL_INVALID_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
|
// GetGlUniformBlockIndex, not GetUniformBlockIndex: the latter answers in the internal
|
||||||
|
// block space, which also resolves storage blocks and the synthesized atomic counter
|
||||||
|
// blocks. Neither is a uniform block (GL 4.6 core 7.6), so both are GL_INVALID_INDEX here.
|
||||||
|
const auto index = programObject->GetGlUniformBlockIndex(uniformBlockName);
|
||||||
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
@@ -1709,7 +1836,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Program object" + std::to_string(program) + " that has been linked."));
|
"Program object" + std::to_string(program) + " that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1720,8 +1847,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::to_string(program) + "."));
|
std::to_string(program) + "."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
||||||
|
// block-keyed tables still carry; translate before touching them.
|
||||||
|
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
||||||
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
||||||
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
|
programObject->SetUniformBlockBinding(blockIndex, uniformBlockBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
|
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
|
||||||
@@ -1733,7 +1863,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Program object" + std::to_string(program) + " that has been linked."));
|
"Program object" + std::to_string(program) + " that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1744,61 +1874,68 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::to_string(program) + "."));
|
std::to_string(program) + "."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
||||||
|
// block-keyed tables still carry; every accessor below is indexed by the block space.
|
||||||
|
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_UNIFORM_BLOCK_DATA_SIZE: {
|
case GL_UNIFORM_BLOCK_DATA_SIZE: {
|
||||||
*params = (GLint)programObject->GetUBOSizeAt(uniformBlockIndex);
|
*params = (GLint)programObject->GetUBOSizeAt(blockIndex);
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_NAME_LENGTH: {
|
case GL_UNIFORM_BLOCK_NAME_LENGTH: {
|
||||||
*params = (GLint)programObject->GetUniformBlockName(uniformBlockIndex).length() + 1;
|
*params = (GLint)programObject->GetUniformBlockName(blockIndex).length() + 1;
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
|
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
|
||||||
*params = programObject->GetUniformBlockActiveUniformCount(uniformBlockIndex);
|
*params = programObject->GetUniformBlockActiveUniformCount(blockIndex);
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_BINDING: {
|
case GL_UNIFORM_BLOCK_BINDING: {
|
||||||
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(uniformBlockIndex));
|
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(blockIndex));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangVertex));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangVertex));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
*params =
|
*params =
|
||||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessControl));
|
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessControl));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
*params =
|
*params =
|
||||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessEvaluation));
|
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessEvaluation));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangGeometry));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangGeometry));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangFragment));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangFragment));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangCompute));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangCompute));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
||||||
// Member entries of an arrayed block are recorded against the first instance;
|
// Member entries of an arrayed block are recorded against the first instance;
|
||||||
// every instance of the array reports that shared member set (matches
|
// every instance of the array reports that shared member set (matches
|
||||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
||||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
|
//
|
||||||
|
// Both sides of the comparison are BLOCK indices: GetUniformBlockMemberOwnerIndex
|
||||||
|
// answers in that space, so the scan uses GetActiveUniformOwnerBlockIndex rather
|
||||||
|
// than the GL_UNIFORM_BLOCK-space GetActiveUniformBlockIndex.
|
||||||
|
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(blockIndex));
|
||||||
GLint uniformIndexCount = 0;
|
GLint uniformIndexCount = 0;
|
||||||
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
||||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
|
if (programObject->GetActiveUniformOwnerBlockIndex(uniformIndex) != ownerIndex) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
||||||
@@ -1807,7 +1944,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
@@ -1828,7 +1965,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
" is not a program object that has been linked."));
|
" is not a program object that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1838,7 +1975,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"not the index of an active uniform block in program."));
|
"not the index of an active uniform block in program."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& name = programObject->GetUniformBlockName(uniformBlockIndex);
|
const auto& name = programObject->GetUniformBlockName(
|
||||||
|
static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex)));
|
||||||
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
|
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
|
||||||
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
|
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
|
||||||
length ? *length : 0);
|
length ? *length : 0);
|
||||||
@@ -2836,6 +2974,73 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface
|
||||||
|
// already carries, so this is a rename of glGetProgramResourceiv's props onto the older
|
||||||
|
// entry point's - and the two are required to agree, which is only true while both read the
|
||||||
|
// same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe
|
||||||
|
// reading its own uninitialised output.
|
||||||
|
static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) {
|
||||||
|
switch (pname) {
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
||||||
|
outProp = GL_BUFFER_BINDING;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE:
|
||||||
|
outProp = GL_BUFFER_DATA_SIZE;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS:
|
||||||
|
outProp = GL_NUM_ACTIVE_VARIABLES;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES:
|
||||||
|
outProp = GL_ACTIVE_VARIABLES;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_VERTEX_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_GEOMETRY_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_FRAGMENT_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_COMPUTE_SHADER;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) {
|
||||||
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
|
if (!programObject) return;
|
||||||
|
GLenum prop = GL_NONE;
|
||||||
|
if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname is not an active atomic counter buffer property."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vector<GLint> values;
|
||||||
|
if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"bufferIndex is not an active atomic counter buffer index."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params == nullptr) return;
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES is the only multi-value property
|
||||||
|
// here, and the caller sized its array from _ACTIVE_ATOMIC_COUNTERS.
|
||||||
|
for (SizeT i = 0; i < values.size(); ++i) params[i] = values[i];
|
||||||
|
}
|
||||||
|
|
||||||
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
||||||
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
||||||
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
|
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||||
void Uniform1d(GLint location, GLdouble v0);
|
void Uniform1d(GLint location, GLdouble v0);
|
||||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||||
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
||||||
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
||||||
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
|
||||||
|
|
||||||
enum class BlockKind {
|
enum class BlockKind {
|
||||||
Uniform, // a real GL uniform block
|
Uniform, // a real GL uniform block
|
||||||
@@ -81,19 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||||
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||||
// stage inputs/outputs, so those get it here.
|
// stage inputs/outputs, so those get it here.
|
||||||
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
if (!type.isArray || EndsWithZeroSubscript(name)) return name;
|
||||||
return name + "[0]";
|
return name + "[0]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
||||||
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
||||||
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
// `record.arraySize` is already the sized-array/reflected-size resolution; the only
|
||||||
if (type != nullptr && type->isArray()) {
|
// extra rule here is GL's 0 for a runtime-sized array.
|
||||||
if (!type->isSizedArray()) return 0;
|
GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
|
||||||
return type->getOuterArraySize();
|
if (record.type.isArray && !record.type.isSizedArray) return 0;
|
||||||
}
|
return record.arraySize;
|
||||||
return reflectedSize < 1 ? 1 : reflectedSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two spellings name the same resource when they are equal, or differ only by the
|
// Two spellings name the same resource when they are equal, or differ only by the
|
||||||
@@ -174,22 +173,21 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return static_cast<GLint>(element);
|
return static_cast<GLint>(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
|
||||||
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||||
return BlockKind::GlobalUbo;
|
return BlockKind::GlobalUbo;
|
||||||
}
|
}
|
||||||
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||||
const glslang::TType* type = block.getType();
|
if (block.type.isBuffer) return BlockKind::Storage;
|
||||||
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
|
||||||
return BlockKind::Uniform;
|
return BlockKind::Uniform;
|
||||||
}
|
}
|
||||||
|
|
||||||
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||||
// uniform matrices. 0 for a non-matrix.
|
// uniform matrices. 0 for a non-matrix.
|
||||||
GLint MatrixStrideOf(const glslang::TType* type) {
|
GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (!type.isMatrix) return 0;
|
||||||
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
|
||||||
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
|
||||||
constexpr int scalarSize = 4;
|
constexpr int scalarSize = 4;
|
||||||
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
: (strideVectorComponents == 2) ? 2 * scalarSize
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
@@ -197,9 +195,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return (vectorAlignment + 15) & ~15;
|
return (vectorAlignment + 15) & ~15;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint IsRowMajorOf(const glslang::TType* type) {
|
GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (!type.isMatrix) return 0;
|
||||||
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint MappedLocation(Int rawLocation) {
|
GLint MappedLocation(Int rawLocation) {
|
||||||
@@ -227,12 +225,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
||||||
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
||||||
// back to the block's own mask there would restore the over-approximation.
|
// back to the block's own mask there would restore the over-approximation.
|
||||||
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
|
||||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
Int blockCount) {
|
||||||
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||||
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
||||||
for (Int index = 0; index < uniformCount; ++index) {
|
for (Int index = 0; index < uniformCount; ++index) {
|
||||||
const auto& uniform = mutableReflection.getUniform(index);
|
const auto& uniform = reflection.uniformReflection[index];
|
||||||
const Int owner = uniform.index;
|
const Int owner = uniform.index;
|
||||||
if (owner < 0 || owner >= blockCount) continue;
|
if (owner < 0 || owner >= blockCount) continue;
|
||||||
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||||
@@ -250,7 +248,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
||||||
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
||||||
// blocks therefore keep that mask untouched.
|
// blocks therefore keep that mask untouched.
|
||||||
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||||
Int tIndex) {
|
Int tIndex) {
|
||||||
String arrayBase;
|
String arrayBase;
|
||||||
Uint element = 0;
|
Uint element = 0;
|
||||||
@@ -264,15 +262,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
|
||||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
|
||||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
blockInterfaceIndex.assign(blockCount, -1);
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||||
|
|
||||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
const auto& block = reflection.blockReflection[tIndex];
|
||||||
const BlockKind kind = ClassifyBlock(block);
|
const BlockKind kind = ClassifyBlock(block);
|
||||||
blockKind[tIndex] = kind;
|
blockKind[tIndex] = kind;
|
||||||
if (kind == BlockKind::AtomicCounter) {
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
@@ -293,7 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||||
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||||
// GetUniformBlockBinding below).
|
// GetUniformBlockBinding below).
|
||||||
const GLint declared = block.getBinding();
|
const GLint declared = block.binding;
|
||||||
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||||
@@ -307,38 +305,53 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||||
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||||
// with them (which is exactly what the CTS does).
|
// with them (which is exactly what the CTS does).
|
||||||
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
const Int glBlockCount = program.GetGlUniformBlockCount();
|
||||||
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||||
|
// The block-space index the block-keyed accessors want; the two spaces differ
|
||||||
|
// whenever the program also has a storage or atomic counter block, which
|
||||||
|
// glslang files under the same reflection list (no EShReflectionSeparateBuffers).
|
||||||
|
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = program.GetUniformBlockName(glIndex);
|
resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
|
||||||
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
|
||||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
|
||||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
|
||||||
if (tIndex >= 0 && tIndex < blockCount) {
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
|
||||||
stagesFromMembers, tIndex);
|
stagesFromMembers, tIndex);
|
||||||
}
|
}
|
||||||
model.uniformBlocks.push_back(Move(resource));
|
model.uniformBlocks.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
void BuildUniformsAndBufferVariables(ProgramObject& program,
|
||||||
|
const ProgramObject::LinkArtifacts& reflection, Model& model,
|
||||||
const Vector<BlockKind>& blockKind,
|
const Vector<BlockKind>& blockKind,
|
||||||
const Vector<Int>& blockInterfaceIndex) {
|
const Vector<Int>& blockInterfaceIndex) {
|
||||||
const Uint uniformCount = program.GetUniformCount();
|
// Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
|
||||||
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
// uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
|
||||||
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
// active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
|
||||||
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
// this is the only place that does. GL uniforms keep their GL index as their
|
||||||
const glslang::TType* type = refl.getType();
|
// GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
|
||||||
|
// the GL-visible entries in this order preserves the correspondence.
|
||||||
|
const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
||||||
|
for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
|
||||||
|
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
|
||||||
|
const auto& type = refl.type;
|
||||||
const Int owner = refl.index;
|
const Int owner = refl.index;
|
||||||
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||||
? blockKind[owner]
|
? blockKind[owner]
|
||||||
: BlockKind::GlobalUbo;
|
: BlockKind::GlobalUbo;
|
||||||
|
const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
|
||||||
|
// Everything except a buffer variable is enumerated through the GL space, so a
|
||||||
|
// uniform the relaxed parse swept out of it (a declared-but-dead default-block
|
||||||
|
// one) stays out of GL_UNIFORM too.
|
||||||
|
if (kind != BlockKind::Storage && glIndex < 0) continue;
|
||||||
|
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = refl.name;
|
resource.name = refl.name;
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
|
||||||
if (kind == BlockKind::Storage) {
|
if (kind == BlockKind::Storage) {
|
||||||
@@ -366,11 +379,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||||
resource.location = -1;
|
resource.location = -1;
|
||||||
} else {
|
} else {
|
||||||
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
const Uint glUniformIndex = static_cast<Uint>(glIndex);
|
||||||
resource.offset = program.GetActiveUniformOffset(glIndex);
|
resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
|
||||||
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
resource.offset = program.GetActiveUniformOffset(glUniformIndex);
|
||||||
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
|
||||||
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
|
||||||
|
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
|
||||||
// A member of a named uniform block has no location, whatever the
|
// A member of a named uniform block has no location, whatever the
|
||||||
// frontend's own location table says (it hands one out to every uniform
|
// frontend's own location table says (it hands one out to every uniform
|
||||||
// so glUniform* can address block members through the global UBO).
|
// so glUniform* can address block members through the global UBO).
|
||||||
@@ -389,12 +403,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
static_cast<GLuint>(i));
|
static_cast<GLuint>(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
|
||||||
// Members of an arrayed block are reflected once, against instance [0].
|
// Members of an arrayed block are reflected once, against instance [0].
|
||||||
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
// GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
|
||||||
|
// Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
|
||||||
|
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
|
||||||
|
const Int owner = program.GlUniformBlockIndexFromBlock(
|
||||||
|
static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
|
||||||
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
if (model.uniforms[i].blockIndex == owner) {
|
if (model.uniforms[i].blockIndex == owner) {
|
||||||
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,17 +432,13 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||||
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||||
// are not part of its output interface.
|
// are not part of its output interface.
|
||||||
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
|
||||||
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
|
||||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
|
||||||
|
|
||||||
const Int inputCount = mutableReflection.getNumPipeInputs();
|
|
||||||
for (Int index = 0; index < inputCount; ++index) {
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeInput(index);
|
const auto& refl = reflection.pipeInputReflection[index];
|
||||||
const glslang::TType* type = refl.getType();
|
const auto& type = refl.type;
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
@@ -432,10 +446,10 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||||
resource.name = WithArraySuffix(glName, type);
|
resource.name = WithArraySuffix(glName, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.location = program.GetAttributeLocation(refl.name);
|
resource.location = program.GetAttributeLocation(refl.name);
|
||||||
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
if (resource.location < 0) resource.location = MappedLocation(refl.location);
|
||||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
resource.isPerPatch = type.isPatch ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programInputs.push_back(Move(resource));
|
model.programInputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -447,16 +461,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// carries its own layout(location=N)), and a location then manufactures a color
|
// carries its own layout(location=N)), and a location then manufactures a color
|
||||||
// index of 0 where GL requires -1
|
// index of 0 where GL requires -1
|
||||||
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||||
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
const Bool lastStageIsFragment = reflection.lastStageIsFragment;
|
||||||
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
const auto& refl = reflection.pipeOutputReflection[index];
|
||||||
const glslang::TType* type = refl.getType();
|
const auto& type = refl.type;
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = WithArraySuffix(refl.name, type);
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
if (resource.location < 0 || !lastStageIsFragment) {
|
if (resource.location < 0 || !lastStageIsFragment) {
|
||||||
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||||
@@ -467,11 +481,11 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||||
// layout(index = N), which the frag-data maps never saw.
|
// layout(index = N), which the frag-data maps never saw.
|
||||||
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
if (resource.locationIndex == 0 && type.hasIndex) {
|
||||||
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
resource.locationIndex = static_cast<GLint>(type.layoutIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
resource.isPerPatch = type.isPatch ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programOutputs.push_back(Move(resource));
|
model.programOutputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -511,15 +525,14 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
Model BuildModel(ProgramObject& program) {
|
Model BuildModel(ProgramObject& program) {
|
||||||
Model model;
|
Model model;
|
||||||
if (!program.GetLinkStatus()) return model;
|
if (!program.GetLinkStatus()) return model;
|
||||||
const glslang::TProgram* reflection = program.GetReflection();
|
const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
|
||||||
if (reflection == nullptr) return model;
|
|
||||||
model.valid = true;
|
model.valid = true;
|
||||||
|
|
||||||
Vector<BlockKind> blockKind;
|
Vector<BlockKind> blockKind;
|
||||||
Vector<Int> blockInterfaceIndex;
|
Vector<Int> blockInterfaceIndex;
|
||||||
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildStageIO(program, *reflection, model);
|
BuildStageIO(program, reflection, model);
|
||||||
BuildXfb(program, model);
|
BuildXfb(program, model);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Bool ended = false;
|
Bool ended = false;
|
||||||
Bool resultCached = false;
|
Bool resultCached = false;
|
||||||
Uint64 cachedResult = 0;
|
Uint64 cachedResult = 0;
|
||||||
// Transform feedback primitive counter at BeginQuery time.
|
// The transform feedback primitive counter matching this query's target, at
|
||||||
|
// BeginQuery time.
|
||||||
Uint64 counterSnapshot = 0;
|
Uint64 counterSnapshot = 0;
|
||||||
|
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
|
||||||
|
// accounting had reproduced exactly, and how many of those it could not (a
|
||||||
|
// geometry stage amplifies). Their deltas decide whether the CPU result may
|
||||||
|
// stand in for the backend's.
|
||||||
|
Uint64 accountedCaptureDrawSnapshot = 0;
|
||||||
|
Uint64 geometryCaptureDrawSnapshot = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query calls may arrive from any thread (launchers migrate the context
|
// Query calls may arrive from any thread (launchers migrate the context
|
||||||
@@ -122,6 +129,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
g_activeTimeElapsedQueryId = 0;
|
g_activeTimeElapsedQueryId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CPU accounting counter a transform feedback query target reads: what the capture
|
||||||
|
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
|
||||||
|
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
|
||||||
|
// for both targets would report the clamped written count as the generated one.
|
||||||
|
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
|
||||||
|
return target == GL_PRIMITIVES_GENERATED
|
||||||
|
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
|
||||||
|
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
|
||||||
|
// context switch between Begin and End, a counter that never moved) would otherwise
|
||||||
|
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
|
||||||
|
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
|
||||||
|
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
|
||||||
|
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether this ended span's result should come from the CPU accounting rather than from
|
||||||
|
// the backend query it also ran. Three conditions, all necessary:
|
||||||
|
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
|
||||||
|
// one; DirectVulkan never sets the bit and so is untouched by any of this);
|
||||||
|
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
|
||||||
|
// whether or not a capture is active, and the accounting only ever sees capture
|
||||||
|
// draws, so the backend's counter is the more complete answer there;
|
||||||
|
// * the span was fully accounted: at least one capture draw reached the accounting
|
||||||
|
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
|
||||||
|
// span made of those is invisible to it) and none of them amplified through a
|
||||||
|
// geometry stage, which the CPU cannot model.
|
||||||
|
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
|
||||||
|
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
|
||||||
|
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
|
||||||
|
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
|
||||||
|
queryObject->geometryCaptureDrawSnapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
|
||||||
|
queryObject->accountedCaptureDrawSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
// Shared GetQueryObject* implementation. Returns false when an error
|
// Shared GetQueryObject* implementation. Returns false when an error
|
||||||
// was recorded and no value should be written back. `outValueProduced`, when given,
|
// was recorded and no value should be written back. `outValueProduced`, when given,
|
||||||
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
|
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
|
||||||
@@ -407,7 +454,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||||
queryObject->backendHandle =
|
queryObject->backendHandle =
|
||||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||||
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||||
|
queryObject->accountedCaptureDrawSnapshot =
|
||||||
|
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
||||||
|
queryObject->geometryCaptureDrawSnapshot =
|
||||||
|
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
||||||
} else if (isOcclusionQuery) {
|
} else if (isOcclusionQuery) {
|
||||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||||
} else {
|
} else {
|
||||||
@@ -448,12 +499,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||||
}
|
}
|
||||||
// Result comes from the GPU query at read time.
|
}
|
||||||
} else {
|
// A backend query that is not going to be read is released here, not left to be
|
||||||
queryObject->cachedResult =
|
// collected later: the span is over, the driver object has nothing left to say.
|
||||||
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
// Ending it first is what makes that legal.
|
||||||
|
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
|
||||||
|
if (queryObject->backendHandle) {
|
||||||
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->backendHandle = nullptr;
|
||||||
|
}
|
||||||
|
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
|
||||||
queryObject->resultCached = true;
|
queryObject->resultCached = true;
|
||||||
}
|
}
|
||||||
|
// Otherwise the result comes from the GPU query at read time.
|
||||||
queryObject->active = false;
|
queryObject->active = false;
|
||||||
queryObject->ended = true;
|
queryObject->ended = true;
|
||||||
activeQueryId = 0;
|
activeQueryId = 0;
|
||||||
@@ -505,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
queryObject->ended = true;
|
queryObject->ended = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BeginConditionalRender(GLuint id, GLenum mode) {
|
||||||
|
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
|
||||||
|
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
|
||||||
|
// treating it as its whole-framebuffer sibling is what an implementation without region
|
||||||
|
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
|
||||||
|
// obligation - see the resolve below.
|
||||||
|
Bool inverted = false;
|
||||||
|
switch (mode) {
|
||||||
|
case GL_QUERY_WAIT:
|
||||||
|
case GL_QUERY_NO_WAIT:
|
||||||
|
case GL_QUERY_BY_REGION_WAIT:
|
||||||
|
case GL_QUERY_BY_REGION_NO_WAIT:
|
||||||
|
inverted = false;
|
||||||
|
break;
|
||||||
|
case GL_QUERY_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_NO_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_BY_REGION_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
|
||||||
|
inverted = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MG_State::pGLContext->IsConditionalRenderActive()) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
const auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
// A generated NAME is not yet a query object; it becomes one at its first use with a
|
||||||
|
// target (the same rule glIsQuery answers by).
|
||||||
|
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->active) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
|
||||||
|
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
|
"Conditional rendering requires an occlusion query object.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
|
||||||
|
// render instead of stalling, so always waiting is conforming and is the only choice that
|
||||||
|
// gives the whole block one deterministic verdict. Reading it per command instead would
|
||||||
|
// let a result that lands mid-block change the answer half way through.
|
||||||
|
Uint64 samplesPassed = 0;
|
||||||
|
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
|
||||||
|
const Bool passed = samplesPassed != 0;
|
||||||
|
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndConditionalRender() {
|
||||||
|
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->EndConditionalRender();
|
||||||
|
}
|
||||||
|
|
||||||
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return;
|
return;
|
||||||
@@ -648,4 +777,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
GetQueryiv(target, pname, params);
|
GetQueryiv(target, pname, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DestroyAllQueryObjects() {
|
||||||
|
// Detach the registry under the lock, release outside it - same discipline
|
||||||
|
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
|
||||||
|
// this drain, every query the app left undeleted survived full library
|
||||||
|
// teardown in the process-global registry: the objects and their backend
|
||||||
|
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
|
||||||
|
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
|
||||||
|
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
|
||||||
|
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
|
||||||
|
UnorderedMap<GLuint, QueryObject*> orphans;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
orphans.swap(g_liveQueryObjects);
|
||||||
|
g_activeTimeElapsedQueryId = 0;
|
||||||
|
g_activePrimitivesWrittenQueryId = 0;
|
||||||
|
g_activePrimitivesGeneratedQueryId = 0;
|
||||||
|
g_activeSamplesPassedQueryId = 0;
|
||||||
|
}
|
||||||
|
if (orphans.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Backend handles must be released by the backend that created them, so
|
||||||
|
// this runs while the function table is still populated. Both backends'
|
||||||
|
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
|
||||||
|
// or ES context is already gone frees only the wrapper.
|
||||||
|
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||||
|
for (const auto& [_, queryObject] : orphans) {
|
||||||
|
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
delete queryObject;
|
||||||
|
}
|
||||||
|
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -29,4 +29,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void QueryCounter(GLuint id, GLenum target);
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
|
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
|
||||||
|
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
|
||||||
|
// with the lock that guards it - lives in this file.
|
||||||
|
void BeginConditionalRender(GLuint id, GLenum mode);
|
||||||
|
void EndConditionalRender();
|
||||||
|
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||||
|
// GL requires queries to die with their context; called only from full library
|
||||||
|
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||||
|
// process-global registry can be drained wholesale. Must run while the backend
|
||||||
|
// function table is still populated: each backend handle has to be released by
|
||||||
|
// the backend that created it, never by a later re-initialized one (whose
|
||||||
|
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
|
||||||
|
// Same contract as DestroyAllSyncObjects.
|
||||||
|
void DestroyAllQueryObjects();
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||||
if (target != GL_BLEND) {
|
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||||
|
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||||
|
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||||
|
GLuint limit = 0;
|
||||||
|
const char* indexName = nullptr;
|
||||||
|
switch (target) {
|
||||||
|
case GL_BLEND:
|
||||||
|
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||||
|
indexName = "Buffer";
|
||||||
|
break;
|
||||||
|
case GL_SCISSOR_TEST:
|
||||||
|
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||||
|
indexName = "Viewport";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
"Only GL_BLEND is supported for indexed capability state."));
|
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||||
|
"capability state."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
"MG_Impl/GLImpl", functionName,
|
String(indexName) + " index " + std::to_string(index) +
|
||||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
"."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||||
|
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||||
|
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||||
|
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||||
|
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||||
|
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||||
|
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||||
|
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"Viewport index " + std::to_string(index) +
|
||||||
|
" is out of range. Max supported is " +
|
||||||
|
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||||
|
if (count < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||||
|
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||||
|
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||||
|
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"first (" + std::to_string(first) + ") + count (" +
|
||||||
|
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||||
|
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||||
|
if (width >= T(0) && height >= T(0)) return true;
|
||||||
|
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||||
|
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||||
|
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||||
|
// error queue holds exactly one entry.
|
||||||
|
template <typename T>
|
||||||
|
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||||
|
")."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||||
|
if (v != nullptr) return true;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||||
::MobileGL::BlendEquation& outEquation) {
|
::MobileGL::BlendEquation& outEquation) {
|
||||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||||
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------ ARB_viewport_array setters ------------------
|
||||||
|
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||||
|
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||||
|
if (count == 0) return;
|
||||||
|
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||||
|
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||||
|
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||||
|
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||||
|
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||||
|
|
||||||
|
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||||
|
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||||
|
if (count == 0) return;
|
||||||
|
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||||
|
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||||
|
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||||
|
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||||
|
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||||
|
|
||||||
|
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||||
|
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||||
|
if (count == 0) return;
|
||||||
|
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||||
|
first + static_cast<GLuint>(i),
|
||||||
|
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||||
|
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||||
|
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||||
|
|
||||||
|
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||||
|
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||||
|
}
|
||||||
|
|
||||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
Bool applyFront = false;
|
Bool applyFront = false;
|
||||||
Bool applyBack = false;
|
Bool applyBack = false;
|
||||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||||
}
|
}
|
||||||
@@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
GLint values[4] = {};
|
GLint values[4] = {};
|
||||||
GetIntegeri_v(target, index, values);
|
GetIntegeri_v(target, index, values);
|
||||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||||
|
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||||
|
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||||
|
? 4
|
||||||
|
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsEnabled_State(GLenum cap) {
|
GLboolean IsEnabled_State(GLenum cap) {
|
||||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Disablei_State(GLenum target, GLuint index) {
|
void Disablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Enablei_State(GLenum target, GLuint index) {
|
void Enablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Viewport_State(x, y, width, height);
|
Viewport_State(x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||||
|
ViewportArrayv_State(first, count, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||||
|
ViewportIndexedf_State(index, x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||||
|
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||||
|
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||||
|
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||||
|
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||||
|
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||||
|
ScissorArrayv_State(first, count, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||||
|
ScissorIndexed_State(index, left, bottom, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||||
|
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||||
|
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||||
|
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||||
|
DepthRangeArrayv_State(first, count, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||||
|
DepthRangeIndexed_State(index, n, f);
|
||||||
|
}
|
||||||
|
|
||||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void Enablei(GLenum target, GLuint index);
|
void Enablei(GLenum target, GLuint index);
|
||||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||||
|
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||||
|
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||||
|
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||||
|
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||||
|
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||||
|
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||||
|
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||||
|
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||||
|
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||||
|
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_Sync.h"
|
#include "GL_Sync.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -35,6 +36,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||||
|
// GL 4.6 core 4.1.2: GL_SYNC_GPU_COMMANDS_COMPLETE is the only condition and the only
|
||||||
|
// legal flags value is zero; both violations return 0 rather than a handle. A caller that
|
||||||
|
// then hands the 0 back to glDeleteSync hits the glDeleteSync(0) no-op below.
|
||||||
|
if (condition != GL_SYNC_GPU_COMMANDS_COMPLETE) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"condition must be GL_SYNC_GPU_COMMANDS_COMPLETE."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (flags != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "flags must be zero."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
auto* syncObject = new SyncObject;
|
auto* syncObject = new SyncObject;
|
||||||
syncObject->condition = condition;
|
syncObject->condition = condition;
|
||||||
syncObject->flags = flags;
|
syncObject->flags = flags;
|
||||||
@@ -64,6 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
|
// GL 4.6 core 4.1.2: the server-side wait takes no flags and no finite timeout - both
|
||||||
|
// arguments exist only to be forward-compatible, and anything else is INVALID_VALUE.
|
||||||
|
// Neither backend ever honored a nonzero timeout (DirectGLES hard-codes
|
||||||
|
// 0/GL_TIMEOUT_IGNORED, DirectVulkan's queue ordering makes the wait implicit), so
|
||||||
|
// rejecting the call loses no wait that used to happen.
|
||||||
|
if (flags != 0 || timeout != GL_TIMEOUT_IGNORED) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"flags must be zero and timeout must be GL_TIMEOUT_IGNORED."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto* syncObject = FindSyncObject(sync);
|
const auto* syncObject = FindSyncObject(sync);
|
||||||
if (!syncObject) {
|
if (!syncObject) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum format, GLenum type, const void* pixels);
|
GLenum format, GLenum type, const void* pixels);
|
||||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
||||||
|
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||||
|
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
|
||||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||||
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||||
Bool ValidateTextureTarget(TextureTarget target) {
|
Bool ValidateTextureTarget(TextureTarget target) {
|
||||||
@@ -312,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
|
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
|
||||||
if (format == TextureInputFormat::StencilIndex) {
|
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
|
||||||
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
|
// STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
|
||||||
|
// and expects INVALID_OPERATION).
|
||||||
|
if (format == TextureInputFormat::StencilIndex &&
|
||||||
|
internalFormat != TextureInternalFormat::StencilIndex8) {
|
||||||
|
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
|
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
|
||||||
@@ -353,6 +358,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||||
|
const char* caller) {
|
||||||
|
// A null object is somebody else's error to report - ValidateTextureObject runs
|
||||||
|
// first at every call site and has already recorded it.
|
||||||
|
if (!textureObject) return false;
|
||||||
|
|
||||||
|
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||||
|
if (mipmapTexture == nullptr) {
|
||||||
|
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
|
||||||
|
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
|
||||||
|
// set). Declining here is not the error code the spec asks for - that would be
|
||||||
|
// INVALID_ENUM from a target check this validator is not - but it does keep a
|
||||||
|
// texture with no image levels whatsoever from reaching a backend that would
|
||||||
|
// dereference a backend texture it never created.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture has no mipmap levels to address."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What this number is, exactly, because two other things are almost it and neither is
|
||||||
|
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
|
||||||
|
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
|
||||||
|
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
|
||||||
|
//
|
||||||
|
// The guarantee that matters is one-sided: this count is always >= the level count the
|
||||||
|
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
|
||||||
|
// with a non-positive extent, so it can only be shorter). That is the safe direction -
|
||||||
|
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
|
||||||
|
// exact match, so the backends keep their own range guard for the band in between: a
|
||||||
|
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
|
||||||
|
// declined by the backend, which is a silent no-op rather than a copy. That band is a
|
||||||
|
// backend storage limitation, not a validation one - rejecting it here with
|
||||||
|
// INVALID_VALUE would be refusing a copy the spec permits.
|
||||||
|
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||||
|
|
||||||
|
if (levelCount == 0) {
|
||||||
|
// No image has ever been defined on this texture, so the fault is the texture,
|
||||||
|
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
|
||||||
|
// copy names is an incomplete texture.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture has no image defined at any level."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture level does not exist in this texture."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||||
if (!textureObject) {
|
if (!textureObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -458,26 +520,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
CopyImageTexelBlock block{};
|
||||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
if (compressedFormat != GL_NONE) {
|
||||||
if (unsizedFormat1 != unsizedFormat2) {
|
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||||
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
if (info.blockByteSize != 0) {
|
||||||
// std::format() call whose format string was the component name, so every
|
block.byteSize = info.blockByteSize;
|
||||||
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
block.blockWidth = info.blockWidth;
|
||||||
// hand over component/function/message separately.
|
block.blockHeight = info.blockHeight;
|
||||||
|
block.compressed = true;
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||||
|
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||||
|
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||||
|
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||||
|
// copy between them would in fact preserve the bytes.
|
||||||
|
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||||
|
const CopyImageTexelBlock& dstBlock) {
|
||||||
|
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||||
|
"A copied image has no storage whose texel size is known."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
"formats are not copy-compatible.",
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
srcBlock.byteSize, dstBlock.byteSize)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||||
|
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||||
|
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||||
|
if (srcBlock.compressed && dstBlock.compressed &&
|
||||||
|
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||||
|
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||||
|
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||||
|
dstBlock.blockHeight)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||||
|
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||||
|
if (!block.compressed) return true;
|
||||||
|
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||||
|
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||||
|
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||||
|
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||||
|
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||||
|
// whole even when the last block is partial.
|
||||||
|
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||||
|
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||||
|
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||||
|
if (originAligned && widthOk && heightOk) return true;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||||
|
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||||
|
"grid of a {} x {} image.",
|
||||||
|
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureInternalFormat internalFormat,
|
TextureInternalFormat internalFormat,
|
||||||
TexturePixelDataType type);
|
TexturePixelDataType type);
|
||||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||||
|
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
|
||||||
|
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
|
||||||
|
// about the object. Entry points that resolve a level straight into a backend image
|
||||||
|
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
|
||||||
|
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
|
||||||
|
// Note the error split is per-entry-point, so this is not universally reusable:
|
||||||
|
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
|
||||||
|
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
|
||||||
|
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||||
|
const char* caller);
|
||||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||||
@@ -40,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureTarget target);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||||
|
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||||
|
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||||
|
struct CopyImageTexelBlock {
|
||||||
|
SizeT byteSize = 0;
|
||||||
|
Uint blockWidth = 1;
|
||||||
|
Uint blockHeight = 1;
|
||||||
|
Bool compressed = false;
|
||||||
|
};
|
||||||
|
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||||
|
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||||
|
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||||
|
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||||
|
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||||
|
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||||
|
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||||
|
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||||
|
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||||
|
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||||
|
const CopyImageTexelBlock& dstBlock);
|
||||||
|
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||||
|
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||||
|
// the edge of the image.
|
||||||
|
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||||
|
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||||
// the requested internalformat asks for, but may supply more.
|
// the requested internalformat asks for, but may supply more.
|
||||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||||
|
|||||||
@@ -514,10 +514,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
||||||
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
||||||
//
|
//
|
||||||
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
// Whether the backend can FEED it at full precision is detected, not assumed: DirectVulkan
|
||||||
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
// needs shaderFloat64, and DirectGLES can never have it at all. What that costs is PRECISION,
|
||||||
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
// not the call and no longer the array: GL 4.6 core 10.3.2 defines no error for a well-formed
|
||||||
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
// glVertexAttribLFormat, and a GL 4.3 context has 64-bit attributes in core, so declining the
|
||||||
|
// call would be non-conformant and would make the four pure state queries
|
||||||
|
// (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET) unanswerable
|
||||||
|
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore RECORDED here and
|
||||||
|
// the array is NARROWED to float32 at draw, matching the fp64 demotion every shader already
|
||||||
|
// gets (DemoteFloat64Pass) - loudly, once, naming the cost. The matching startup POST row is in
|
||||||
|
// MG_Util/SelfTest/DriverPost.cpp; the draw-side narrowing is DirectGLES/Managers.cpp and, on
|
||||||
|
// DirectVulkan, VertexInputStateFactory's Float64 case.
|
||||||
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
GLuint attribindex, GLint size, GLenum type,
|
GLuint attribindex, GLint size, GLenum type,
|
||||||
GLuint relativeoffset) {
|
GLuint relativeoffset) {
|
||||||
@@ -527,15 +534,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (!MG_Backend::pActiveBackendObject ||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||||
"backend has no double-precision vertex attribute support - see the "
|
"backend has no double-precision vertex attribute support - the format is recorded "
|
||||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
"and queryable, and the array is FETCHED AT FLOAT32 PRECISION at draw (the same "
|
||||||
|
"narrowing the shader's dvec inputs already get); see the \"64-bit vertex "
|
||||||
|
"attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||||
attribindex);
|
attribindex);
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
|
||||||
"64-bit vertex attributes are not supported by this backend."));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
||||||
|
|||||||
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
|||||||
|
|
||||||
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||||
MGLOG_W("glx: glXCopyContext is not supported");
|
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||||
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||||
MGLOG_W("glx: glXCreatePixmap is not supported");
|
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||||
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||||
MGLOG_W("glx: glXUseXFont is not supported");
|
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
||||||
}
|
}
|
||||||
if (!fns->Valid()) {
|
if (!fns->Valid()) {
|
||||||
MGLOG_E("glx: failed to load libX11 entry points");
|
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
||||||
}
|
}
|
||||||
return fns;
|
return fns;
|
||||||
}();
|
}();
|
||||||
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
Uint32 width = 0;
|
Uint32 width = 0;
|
||||||
Uint32 height = 0;
|
Uint32 height = 0;
|
||||||
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
||||||
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||||
width, height);
|
width, height);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||||
EGLDisplay display = EnsureDisplay();
|
EGLDisplay display = EnsureDisplay();
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
MGLOG_E("glx: no EGL display");
|
MGLOG_E_ONCE("glx: no EGL display");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||||
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
||||||
configCount <= 0) {
|
configCount <= 0) {
|
||||||
MGLOG_E("glx: eglChooseConfig failed");
|
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||||
if (eglContext == EGL_NO_CONTEXT) {
|
if (eglContext == EGL_NO_CONTEXT) {
|
||||||
MGLOG_E("glx: eglCreateContext failed");
|
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
|
|
||||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
||||||
object->Context)) {
|
object->Context)) {
|
||||||
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
t_current = {dpy, drawable, drawable, context};
|
t_current = {dpy, drawable, drawable, context};
|
||||||
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
if (context && draw != read) {
|
if (context && draw != read) {
|
||||||
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
||||||
// drawable for both, which is what every real caller here needs.
|
// drawable for both, which is what every real caller here needs.
|
||||||
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||||
read);
|
read);
|
||||||
}
|
}
|
||||||
const int result = MakeCurrent(dpy, draw, context);
|
const int result = MakeCurrent(dpy, draw, context);
|
||||||
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
auto& surfaces = DrawableSurfaces();
|
auto& surfaces = DrawableSurfaces();
|
||||||
auto it = surfaces.find(drawable);
|
auto it = surfaces.find(drawable);
|
||||||
if (it == surfaces.end()) {
|
if (it == surfaces.end()) {
|
||||||
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SyncSurfaceSize(dpy, drawable, it->second);
|
SyncSurfaceSize(dpy, drawable, it->second);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
|
|||||||
#endif
|
#endif
|
||||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
MGLOG_D("Failed to get function: %s", (const char*)name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
|
|||||||
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
||||||
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
||||||
|
|
||||||
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
|
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Impl
|
} // namespace MobileGL::MG_Impl
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
}
|
}
|
||||||
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
||||||
if (!metalLayerClass) {
|
if (!metalLayerClass) {
|
||||||
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
|
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
static_cast<GLint>(geometry.DrawableSize.width),
|
static_cast<GLint>(geometry.DrawableSize.width),
|
||||||
static_cast<GLint>(geometry.DrawableSize.height));
|
static_cast<GLint>(geometry.DrawableSize.height));
|
||||||
if (error != kCGLNoError) {
|
if (error != kCGLNoError) {
|
||||||
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
}
|
}
|
||||||
const auto error = CGLImpl::SetCurrentContext(context);
|
const auto error = CGLImpl::SetCurrentContext(context);
|
||||||
if (error != kCGLNoError) {
|
if (error != kCGLNoError) {
|
||||||
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
}
|
}
|
||||||
const auto error = CGLImpl::FlushDrawable(context);
|
const auto error = CGLImpl::FlushDrawable(context);
|
||||||
if (error != kCGLNoError) {
|
if (error != kCGLNoError) {
|
||||||
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
static_cast<GLint>(geometry.DrawableSize.width),
|
static_cast<GLint>(geometry.DrawableSize.width),
|
||||||
static_cast<GLint>(geometry.DrawableSize.height));
|
static_cast<GLint>(geometry.DrawableSize.height));
|
||||||
if (error != kCGLNoError) {
|
if (error != kCGLNoError) {
|
||||||
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CGLImpl::UpdateContext(context);
|
CGLImpl::UpdateContext(context);
|
||||||
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
SEL selector = sel_registerName(selectorName);
|
SEL selector = sel_registerName(selectorName);
|
||||||
Method method = class_getInstanceMethod(cls, selector);
|
Method method = class_getInstanceMethod(cls, selector);
|
||||||
if (!method) {
|
if (!method) {
|
||||||
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
|
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (original) {
|
if (original) {
|
||||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
SEL selector = sel_registerName(selectorName);
|
SEL selector = sel_registerName(selectorName);
|
||||||
Method method = class_getClassMethod(cls, selector);
|
Method method = class_getClassMethod(cls, selector);
|
||||||
if (!method) {
|
if (!method) {
|
||||||
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
|
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
method_setImplementation(method, replacement);
|
method_setImplementation(method, replacement);
|
||||||
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||||
if (!pixelFormatClass || !contextClass) {
|
if (!pixelFormatClass || !contextClass) {
|
||||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||||
MGLOG_W("wglCopyContext is not supported");
|
MGLOG_W_ONCE("wglCopyContext is not supported");
|
||||||
SetLastError(ERROR_NOT_SUPPORTED);
|
SetLastError(ERROR_NOT_SUPPORTED);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
|||||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
LPGLYPHMETRICSFLOAT) {
|
LPGLYPHMETRICSFLOAT) {
|
||||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
LPGLYPHMETRICSFLOAT) {
|
LPGLYPHMETRICSFLOAT) {
|
||||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
Uint32 width = 0;
|
Uint32 width = 0;
|
||||||
Uint32 height = 0;
|
Uint32 height = 0;
|
||||||
if (!QueryClientSize(hwnd, width, height)) {
|
if (!QueryClientSize(hwnd, width, height)) {
|
||||||
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
|
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
EGLSurface surface =
|
EGLSurface surface =
|
||||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||||
EGLDisplay display = EnsureDisplay();
|
EGLDisplay display = EnsureDisplay();
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
MGLOG_E("wgl: no EGL display");
|
MGLOG_E_ONCE("wgl: no EGL display");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
EGLConfig config = nullptr;
|
EGLConfig config = nullptr;
|
||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
||||||
MGLOG_E("wgl: eglChooseConfig failed");
|
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||||
if (eglContext == EGL_NO_CONTEXT) {
|
if (eglContext == EGL_NO_CONTEXT) {
|
||||||
MGLOG_E("wgl: eglCreateContext failed");
|
MGLOG_E_ONCE("wgl: eglCreateContext failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
auto& surfaces = WindowSurfaces();
|
auto& surfaces = WindowSurfaces();
|
||||||
auto it = surfaces.find(hwnd);
|
auto it = surfaces.find(hwnd);
|
||||||
if (it == surfaces.end()) {
|
if (it == surfaces.end()) {
|
||||||
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
|
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
SyncSurfaceSize(hwnd, it->second);
|
SyncSurfaceSize(hwnd, it->second);
|
||||||
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
||||||
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
t_current = {hdc, hglrc};
|
t_current = {hdc, hglrc};
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
|
|
||||||
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||||
|
|
||||||
# Only meaningful where MobileGL_s exists (i.e. not Android).
|
# Desktop links the static implementation directly. Android runs the same
|
||||||
if (NOT TARGET MobileGL_s)
|
# executable from adb shell and links the shipping shared library instead.
|
||||||
message(STATUS "MobileGL_s is not available; skipping the integration test module")
|
if (ANDROID)
|
||||||
|
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
|
||||||
|
elseif (TARGET MobileGL_s)
|
||||||
|
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
|
||||||
|
else()
|
||||||
|
message(STATUS "No MobileGL library target is available; skipping the integration test module")
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -54,23 +59,52 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
|
Scenarios/SnormAttachmentScenario.cpp
|
||||||
Scenarios/PipelineFailureScenario.cpp
|
Scenarios/PipelineFailureScenario.cpp
|
||||||
Scenarios/AdvertisedLimitsScenario.cpp
|
Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
Scenarios/PixelStoreSweepScenario.cpp
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
Scenarios/FragCoordOriginScenario.cpp
|
Scenarios/FragCoordOriginScenario.cpp
|
||||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||||
Scenarios/DepthStencilReadbackScenario.cpp
|
Scenarios/DepthStencilReadbackScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||||
|
Scenarios/ClipDistanceScenario.cpp
|
||||||
|
Scenarios/ViewportArrayScenario.cpp
|
||||||
Scenarios/SsboArrayLengthScenario.cpp
|
Scenarios/SsboArrayLengthScenario.cpp
|
||||||
Scenarios/DoublePrecisionScenario.cpp
|
Scenarios/DoublePrecisionScenario.cpp
|
||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
|
Scenarios/IterationRPFirstReductionScenario.cpp
|
||||||
|
Scenarios/IterationRPProgram203Scenario.cpp
|
||||||
|
Scenarios/IterationRPScratchFixScenario.cpp
|
||||||
Scenarios/ProgramPipelineScenario.cpp
|
Scenarios/ProgramPipelineScenario.cpp
|
||||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||||
|
Scenarios/ImageTargetKindScenario.cpp
|
||||||
|
Scenarios/ImageFormatQualifierScenario.cpp
|
||||||
|
Scenarios/NonCoreImageFormatScenario.cpp
|
||||||
|
Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||||
Scenarios/SsboDeclarationFormScenario.cpp
|
Scenarios/SsboDeclarationFormScenario.cpp
|
||||||
Scenarios/Glsl420DeclarationScenario.cpp
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
|
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||||
|
Scenarios/TessellationDrawModeScenario.cpp
|
||||||
|
Scenarios/GeometryDrawModeScenario.cpp
|
||||||
|
Scenarios/PostLinkAttachScenario.cpp
|
||||||
|
Scenarios/FormatlessImageBakeScenario.cpp
|
||||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||||
Scenarios/BufferTextureScenario.cpp
|
Scenarios/BufferTextureScenario.cpp
|
||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.cpp
|
||||||
|
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||||
|
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||||
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
|
Scenarios/PackedWordReadbackScenario.cpp
|
||||||
|
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||||
|
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||||
|
Scenarios/AtomicCounterScenario.cpp
|
||||||
|
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||||
|
Scenarios/StorageBufferRegrowScenario.cpp
|
||||||
|
Scenarios/RelinkStageSetScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
@@ -81,9 +115,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
|
|||||||
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
GTest::gtest
|
GTest::gtest
|
||||||
MobileGL_s
|
${MGL_ITEST_MOBILEGL_TARGET}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
|
||||||
|
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
|
||||||
|
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
|
||||||
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
|
${MGL_ITEST_ANDROID_LIBRARY}
|
||||||
|
${MGL_ITEST_LOG_LIBRARY}
|
||||||
|
${MGL_ITEST_MEDIANDK_LIBRARY}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
||||||
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
||||||
@@ -92,6 +137,10 @@ if (MSVC)
|
|||||||
endif()
|
endif()
|
||||||
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- ctest wiring --------------------------------------------------------
|
# --- ctest wiring --------------------------------------------------------
|
||||||
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
||||||
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
||||||
@@ -212,6 +261,19 @@ endif()
|
|||||||
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
||||||
if (MOBILEGL_ITEST_VK_ICD)
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
|
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
|
||||||
|
# The three iterationRP repairs are tri-state quirks that default to device
|
||||||
|
# auto-detection, and lavapipe is not on any auto list - so on lavapipe the
|
||||||
|
# iterationRP scenarios run unrepaired and Program 203 misses its golden
|
||||||
|
# output. CI's integration-gpu job exports these three by hand; pinning them
|
||||||
|
# to the ICD instead means a local `ctest -L integration-gpu` measures the
|
||||||
|
# same thing the gate does, with no environment to remember.
|
||||||
|
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
||||||
|
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
||||||
|
list(APPEND MGL_ITEST_VULKAN_ENV
|
||||||
|
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||||
|
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
|
||||||
|
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
||||||
@@ -238,6 +300,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
|||||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -285,3 +349,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A fourth registration, of the depth/stencil readback scenarios, with the ES
|
||||||
|
# shader-sampling emulation forced on. Not paranoia - without it these scenarios are
|
||||||
|
# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil
|
||||||
|
# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them
|
||||||
|
# goes green through a native path that the Adreno device does not have. Deleting the
|
||||||
|
# entire emulation left all of them passing. With the flag the native spellings are off
|
||||||
|
# the table and only the path the device actually takes remains. DirectGLES only - the
|
||||||
|
# emulation is DirectGLES's.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation."
|
||||||
|
TEST_FILTER "DepthStencilReadback*Scenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,6 +15,16 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
#include <android/hardware_buffer.h>
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <media/NdkImage.h>
|
||||||
|
#include <media/NdkImageReader.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
|
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
|
||||||
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
||||||
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
||||||
@@ -32,7 +42,7 @@
|
|||||||
// the only construction that is actually predictive here: MobileGL ABORTS
|
// the only construction that is actually predictive here: MobileGL ABORTS
|
||||||
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
||||||
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
||||||
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
|
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||||
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -53,6 +63,83 @@ namespace MGITest {
|
|||||||
constexpr int kSurfaceWidth = 128;
|
constexpr int kSurfaceWidth = 128;
|
||||||
constexpr int kSurfaceHeight = 96;
|
constexpr int kSurfaceHeight = 96;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
HWND g_testWindow = nullptr;
|
||||||
|
|
||||||
|
HWND CreateTestWindow() {
|
||||||
|
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
|
||||||
|
static bool registered = false;
|
||||||
|
if (!registered) {
|
||||||
|
WNDCLASSW windowClass{};
|
||||||
|
windowClass.lpfnWndProc = DefWindowProcW;
|
||||||
|
windowClass.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
windowClass.lpszClassName = kClassName;
|
||||||
|
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
|
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
|
||||||
|
GetModuleHandleW(nullptr), nullptr);
|
||||||
|
}
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
AImageReader* g_imageReader = nullptr;
|
||||||
|
ANativeWindow* g_imageReaderWindow = nullptr;
|
||||||
|
|
||||||
|
void DrainImageReader(void*, AImageReader* reader) {
|
||||||
|
AImage* image = nullptr;
|
||||||
|
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
|
||||||
|
AImage_delete(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CreateImageReaderWindow() {
|
||||||
|
if (g_imageReaderWindow != nullptr) return true;
|
||||||
|
constexpr int kMaxImages = 4;
|
||||||
|
const media_status_t status = AImageReader_newWithUsage(
|
||||||
|
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
||||||
|
kMaxImages, &g_imageReader);
|
||||||
|
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
|
||||||
|
|
||||||
|
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
|
||||||
|
AImageReader_setImageListener(g_imageReader, &listener);
|
||||||
|
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
|
||||||
|
g_imageReaderWindow == nullptr) {
|
||||||
|
AImageReader_setImageListener(g_imageReader, nullptr);
|
||||||
|
AImageReader_delete(g_imageReader);
|
||||||
|
g_imageReader = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ANativeWindow_acquire(g_imageReaderWindow);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyImageReaderWindow() {
|
||||||
|
if (g_imageReaderWindow != nullptr) {
|
||||||
|
ANativeWindow_release(g_imageReaderWindow);
|
||||||
|
g_imageReaderWindow = nullptr;
|
||||||
|
}
|
||||||
|
if (g_imageReader != nullptr) {
|
||||||
|
AImageReader_setImageListener(g_imageReader, nullptr);
|
||||||
|
AImageReader_delete(g_imageReader);
|
||||||
|
g_imageReader = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool UseWindowSurface() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
|
||||||
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string EnvOr(const char* name, const char* fallback) {
|
std::string EnvOr(const char* name, const char* fallback) {
|
||||||
const char* value = std::getenv(name);
|
const char* value = std::getenv(name);
|
||||||
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
||||||
@@ -87,10 +174,10 @@ namespace MGITest {
|
|||||||
// callers). surfaceless is the platform with no window-system dependency at
|
// callers). surfaceless is the platform with no window-system dependency at
|
||||||
// all; the surface this file then creates is still a pbuffer, which every
|
// all; the surface this file then creates is still a pbuffer, which every
|
||||||
// platform supports and which the amendment to this rule requires as the
|
// platform supports and which the amendment to this rule requires as the
|
||||||
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
// fallback shape on desktop. Android instead supplies an AImageReader
|
||||||
|
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||||
// driver that consults them directly cannot reintroduce the dependency
|
// driver that consults them directly cannot reintroduce the dependency
|
||||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
// behind EGL's back.
|
||||||
// for Android, so no device path is affected.
|
|
||||||
void EnsureHeadlessPlatform() {
|
void EnsureHeadlessPlatform() {
|
||||||
#if defined(__linux__) && !defined(__ANDROID__)
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
static bool done = false;
|
static bool done = false;
|
||||||
@@ -134,8 +221,9 @@ namespace MGITest {
|
|||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool useWindowSurface = UseWindowSurface();
|
||||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
||||||
EGL_PBUFFER_BIT,
|
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
|
||||||
EGL_RED_SIZE,
|
EGL_RED_SIZE,
|
||||||
8,
|
8,
|
||||||
EGL_GREEN_SIZE,
|
EGL_GREEN_SIZE,
|
||||||
@@ -152,7 +240,9 @@ namespace MGITest {
|
|||||||
EGLConfig config = nullptr;
|
EGLConfig config = nullptr;
|
||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
||||||
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
outReason = WithEglError(useWindowSurface
|
||||||
|
? "eglChooseConfig found no window-capable RGBA8/D24 config"
|
||||||
|
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +256,32 @@ namespace MGITest {
|
|||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EGLSurface surface = EGL_NO_SURFACE;
|
||||||
|
if (useWindowSurface) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
|
||||||
|
if (g_testWindow == nullptr) {
|
||||||
|
outReason = "failed to create the Windows integration-test window";
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
if (!CreateImageReaderWindow()) {
|
||||||
|
outReason = "failed to create the Android AImageReader integration-test window";
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||||
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||||
|
}
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
outReason = WithEglError("eglCreatePbufferSurface failed");
|
#if defined(__ANDROID__)
|
||||||
|
DestroyImageReaderWindow();
|
||||||
|
#endif
|
||||||
|
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
||||||
|
: "eglCreatePbufferSurface failed");
|
||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
// The step that brings the whole backend up (DirectVulkan creates its
|
// The step that brings the whole backend up (DirectVulkan creates its
|
||||||
@@ -491,6 +603,14 @@ namespace MGITest {
|
|||||||
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
||||||
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
||||||
eglTerminate(display);
|
eglTerminate(display);
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (g_testWindow != nullptr) {
|
||||||
|
DestroyWindow(g_testWindow);
|
||||||
|
g_testWindow = nullptr;
|
||||||
|
}
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
DestroyImageReaderWindow();
|
||||||
|
#endif
|
||||||
m_context = nullptr;
|
m_context = nullptr;
|
||||||
m_surface = nullptr;
|
m_surface = nullptr;
|
||||||
m_display = nullptr;
|
m_display = nullptr;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
// inspects backend state - both bugs this module pins were invisible to
|
// inspects backend state - both bugs this module pins were invisible to
|
||||||
// state-level assertions and visible only in pixels.
|
// state-level assertions and visible only in pixels.
|
||||||
//
|
//
|
||||||
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
|
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
|
||||||
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
|
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
|
||||||
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
|
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
|
||||||
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
|
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
|
||||||
// cross-frame scenarios need to be real).
|
// boundary the cross-frame scenarios need to be real).
|
||||||
//
|
//
|
||||||
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||||
// initialization, so the CMake wiring runs this binary once per backend rather
|
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||||
|
|||||||
@@ -31,8 +31,14 @@ namespace {
|
|||||||
// silently bound to a workstation's window system is a different
|
// silently bound to a workstation's window system is a different
|
||||||
// run from CI's and must be visible as one in the log.
|
// run from CI's and must be visible as one in the log.
|
||||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
#if defined(__ANDROID__)
|
||||||
|
constexpr const char* surfaceKind = "AImageReader window";
|
||||||
|
#else
|
||||||
|
constexpr const char* surfaceKind = "pbuffer";
|
||||||
|
#endif
|
||||||
|
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
|
||||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||||
|
surfaceKind,
|
||||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||||
} else if (MGITest::RequireGpu()) {
|
} else if (MGITest::RequireGpu()) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
|
|||||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
|||||||
"derived component limits are computed in";
|
"derived component limits are computed in";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||||
|
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||||
|
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||||
|
// different source, so all three are checked on both lanes.
|
||||||
|
//
|
||||||
|
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||||
|
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||||
|
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||||
|
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||||
|
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||||
|
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||||
|
GLint maxViewports = -1;
|
||||||
|
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||||
|
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||||
|
"viewport, and the CTS sizes its arrays off this number";
|
||||||
|
|
||||||
|
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||||
|
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||||
|
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||||
|
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||||
|
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||||
|
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||||
|
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||||
|
|
||||||
|
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||||
|
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||||
|
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||||
|
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||||
|
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||||
|
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||||
|
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||||
|
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||||
|
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||||
|
// limit MobileGL currently passes through from the driver.
|
||||||
|
GLint subpixelBits = -1;
|
||||||
|
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||||
|
"a negative value is what a sign-flipped uint32 looks like";
|
||||||
|
|
||||||
|
GLint viewportDims[2] = {-1, -1};
|
||||||
|
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
GLint maxRenderbufferSize = -1;
|
||||||
|
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||||
|
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||||
|
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||||
|
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.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 - ATOMIC COUNTERS, END TO END.
|
||||||
|
//
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
|
||||||
|
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
|
||||||
|
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
|
||||||
|
// closing two open ends that used to be missing entirely -
|
||||||
|
//
|
||||||
|
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
|
||||||
|
// relation to the GL binding point N the application bound its buffer to (and could alias an
|
||||||
|
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
|
||||||
|
// range; and
|
||||||
|
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
|
||||||
|
// read, is re-issued as a shader-storage binding at that reserved slot.
|
||||||
|
//
|
||||||
|
// Neither end alone is observable: with only the first the shader increments a block nobody
|
||||||
|
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
|
||||||
|
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
|
||||||
|
//
|
||||||
|
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
|
||||||
|
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
|
||||||
|
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
|
||||||
|
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
|
||||||
|
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
|
||||||
|
// block is laid out the way GL says": a lowering that packed the members in declaration
|
||||||
|
// order without honouring `offset` would still pass a single-counter check.
|
||||||
|
constexpr const char* kCounterComputeSource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 4) in;
|
||||||
|
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
|
||||||
|
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
|
||||||
|
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
|
||||||
|
void main() {
|
||||||
|
atomicCounterIncrement(g_first);
|
||||||
|
atomicCounterIncrement(g_second);
|
||||||
|
atomicCounterIncrement(g_second);
|
||||||
|
atomicCounterIncrement(g_other);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr int kLocalSizeX = 4;
|
||||||
|
constexpr int kWorkGroups = 2;
|
||||||
|
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
|
||||||
|
|
||||||
|
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
|
||||||
|
// that survives is also proof that the buffer's CPU-side contents reached the driver.
|
||||||
|
constexpr unsigned int kSeedFirst = 5;
|
||||||
|
constexpr unsigned int kSeedSecond = 100;
|
||||||
|
constexpr unsigned int kSeedOther = 7;
|
||||||
|
|
||||||
|
class AtomicCounterScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
GLint counters = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
||||||
|
GLint buffers = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
|
||||||
|
if (counters < 3 || buffers < 2) {
|
||||||
|
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
|
||||||
|
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
|
||||||
|
<< "; this needs 3 and 2";
|
||||||
|
}
|
||||||
|
if (!AtomicCountersAreWired()) {
|
||||||
|
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
|
||||||
|
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
|
||||||
|
<< "is still resolved from the shader-storage binding points";
|
||||||
|
}
|
||||||
|
m_program = CompileComputeProgram(kCounterComputeSource);
|
||||||
|
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
m_buffers.clear();
|
||||||
|
m_program = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
|
||||||
|
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
|
||||||
|
// frontend half (limits, reflection queries, the link-time offset rules) is
|
||||||
|
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
|
||||||
|
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||||
|
|
||||||
|
unsigned int CompileComputeProgram(const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("compute program did not link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
|
||||||
|
// `binding`.
|
||||||
|
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||||
|
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
|
||||||
|
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
m_buffers.push_back(buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
|
||||||
|
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||||
|
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
|
||||||
|
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dispatch() {
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glDispatchCompute(kWorkGroups, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
std::string m_buildLog;
|
||||||
|
std::vector<GLuint> m_buffers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The counter values a dispatch leaves behind, per binding point and per offset within one
|
||||||
|
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
|
||||||
|
// before the wiring landed every one of these read back its seed unchanged.
|
||||||
|
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
|
||||||
|
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
|
||||||
|
|
||||||
|
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
|
||||||
|
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
|
||||||
|
|
||||||
|
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
|
||||||
|
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
|
||||||
|
<< " means the shader's increments never reached the buffer the application bound";
|
||||||
|
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
|
||||||
|
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
|
||||||
|
<< "offset was not carried through the lowering, even though offset 0 was";
|
||||||
|
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
|
||||||
|
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
|
||||||
|
<< "resolves to a different reserved slot and is where an off-by-one shows up";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second dispatch continues from where the first left off, and a re-seed between them is
|
||||||
|
// visible to the shader. Both halves of the buffer's traffic have to work, in both
|
||||||
|
// directions: the increments are only observable through the readback path, and the re-seed
|
||||||
|
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
|
||||||
|
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||||
|
MakeCounterBuffer(1, {0u});
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
Dispatch();
|
||||||
|
std::vector<unsigned int> values = ReadCounters(zero, 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
|
||||||
|
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
|
||||||
|
|
||||||
|
const unsigned int reseed[2] = {1000u, 2000u};
|
||||||
|
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";
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
values = ReadCounters(zero, 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
|
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -73,7 +74,60 @@ out vec4 o_color;
|
|||||||
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
||||||
)";
|
)";
|
||||||
|
|
||||||
class BufferTextureScenario : public ScenarioTest {};
|
// A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
|
||||||
|
// another, so a single dispatch proves the read direction (which already worked) and
|
||||||
|
// the write direction (which is what this exists for) apart from each other.
|
||||||
|
constexpr const char* kImageBufferCS = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 0, rgba8) uniform imageBuffer uImage;
|
||||||
|
void main() {
|
||||||
|
vec4 read = imageLoad(uImage, 1);
|
||||||
|
imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
|
||||||
|
imageStore(uImage, 2, read);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class BufferTextureScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
bool ComputeImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int MakeComputeProgram(const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
|
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
|
||||||
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
|
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
|
||||||
@@ -171,4 +225,173 @@ void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
|||||||
EXPECT_EQ(FirstGLError(), 0u);
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
|
||||||
|
// land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
|
||||||
|
// and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
|
||||||
|
// and the shadow is refreshed on the next read; a buffer reached through an image unit is
|
||||||
|
// the same write through a different binding, and Espryt used to flag only the first, so
|
||||||
|
// an imageStore into a buffer texture was invisible to every CPU read that followed it -
|
||||||
|
// silently, with the correct value sitting in the driver's buffer the whole time.
|
||||||
|
//
|
||||||
|
// The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
|
||||||
|
// failure here cannot be blamed on the image binding not working at all.
|
||||||
|
TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";
|
||||||
|
|
||||||
|
constexpr GLuint kRed = 0x000000ffu; // RGBA8 little-endian: r = 255
|
||||||
|
constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
|
||||||
|
constexpr int kTexels = 16;
|
||||||
|
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
const unsigned int program = MakeComputeProgram(kImageBufferCS);
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
|
||||||
|
const std::vector<GLuint> texels(kTexels, kRed);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
|
||||||
|
GL_DYNAMIC_COPY);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";
|
||||||
|
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
|
||||||
|
// Both CPU read paths, because they are two entry points onto the same refresh and a
|
||||||
|
// fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
|
||||||
|
// ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
|
||||||
|
// context across every scenario in the process, and a leaked buffer or image binding
|
||||||
|
// here would surface as a failure somewhere else entirely.
|
||||||
|
std::vector<GLuint> readBack(kTexels, 0u);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
|
||||||
|
readBack.data());
|
||||||
|
EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
|
||||||
|
EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";
|
||||||
|
|
||||||
|
const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
|
||||||
|
EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
|
||||||
|
if (mapped != nullptr) {
|
||||||
|
GLuint mappedTexel0 = 0;
|
||||||
|
std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
|
||||||
|
EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
|
||||||
|
glUnmapBuffer(GL_TEXTURE_BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
|
||||||
|
// fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
|
||||||
|
// GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
|
||||||
|
// KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
|
||||||
|
// the case before any value is compared.
|
||||||
|
//
|
||||||
|
// The two halves report DIFFERENT units and only one of them is clamped, which is the thing
|
||||||
|
// easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
|
||||||
|
// BUFFER_SIZE is the range in basic machine units exactly as it was given.
|
||||||
|
TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
GLint offsetAlignment = 1;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment < 1) offsetAlignment = 1;
|
||||||
|
GLint maxTexels = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";
|
||||||
|
|
||||||
|
constexpr GLint kTexelBytes = 4; // GL_RGBA8
|
||||||
|
const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
|
||||||
|
const GLsizeiptr rangeBytes = 32 * kTexelBytes;
|
||||||
|
// Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
|
||||||
|
// than out of the texture's window would be caught.
|
||||||
|
const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;
|
||||||
|
|
||||||
|
const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";
|
||||||
|
|
||||||
|
const auto levelQuery = [](GLenum pname) {
|
||||||
|
GLint value = -1;
|
||||||
|
glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
const auto levelQueryF = [](GLenum pname) {
|
||||||
|
GLfloat value = -1.0f;
|
||||||
|
glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
|
||||||
|
<< "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
|
||||||
|
<< "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
|
||||||
|
EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
|
||||||
|
<< "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";
|
||||||
|
|
||||||
|
// The float getter is a separate switch and has drifted from the integer one before.
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";
|
||||||
|
|
||||||
|
// The whole-buffer form follows the buffer's current size instead of freezing a window.
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
|
||||||
|
// the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
|
||||||
|
GLuint plainTexture = 0;
|
||||||
|
glGenTextures(1, &plainTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, plainTexture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
GLint unused = -1;
|
||||||
|
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
|
||||||
|
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glDeleteTextures(1, &plainTexture);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - gl_ClipDistance ACTUALLY CLIPS, AND ONLY WHERE IT IS ENABLED.
|
||||||
|
//
|
||||||
|
// CapabilityInput::ClipDistance0..7 existed end to end - the GL enum converted to it, the
|
||||||
|
// string converter named it, glEnable(GL_CLIP_DISTANCE0 + i) raised no error - and then
|
||||||
|
// RenderState::SetCapability had no case for it and dropped it into `default: break`. Nothing
|
||||||
|
// was stored, no version was bumped, and neither backend ever heard about it. The shader half
|
||||||
|
// worked all along (SPIRV-Cross emits gl_ClipDistance with a
|
||||||
|
// `#extension GL_EXT_clip_cull_distance : require` that Adreno accepts), so the distances were
|
||||||
|
// computed and then ignored: no clipping ever happened on DirectGLES, which is the whole of
|
||||||
|
// KHR-GLxx.clip_distance.functional. glIsEnabled lied about it too - it returned GL_FALSE
|
||||||
|
// immediately after a successful glEnable.
|
||||||
|
//
|
||||||
|
// The assertions are behavioural, not query-shaped, because a query-only test passes against a
|
||||||
|
// backend that stores the bit and never forwards it. Each case draws one full-viewport triangle
|
||||||
|
// whose clip distance is positive on one side of the viewport and negative on the other, then
|
||||||
|
// checks BOTH sides: the kept side proves the draw happened at all, and the clipped side is the
|
||||||
|
// actual claim. The disabled case is the negative control - the identical shader with the
|
||||||
|
// identical distances and the enable turned off must leave both sides painted, which is what
|
||||||
|
// says the pixels below are being removed by clipping and not by something else.
|
||||||
|
//
|
||||||
|
// HONEST LIMIT OF THIS FILE IN CI. Of the four cases, only EnableIsObservableThroughIsEnabled is
|
||||||
|
// falsifiable on the software rasterizers every automated lane runs on. llvmpipe and lavapipe
|
||||||
|
// clip by EVERY declared gl_ClipDistance regardless of the enables, so
|
||||||
|
// AnEnabledClipDistanceRemovesTheNegativeHalf goes green there against the broken tree as well,
|
||||||
|
// and the two cases that need real per-distance semantics skip (see
|
||||||
|
// DriverHonoursPerDistanceEnables). What actually pins the behaviour is Adreno, through
|
||||||
|
// KHR-GLxx.clip_distance.functional - whose "without dynamic redeclaration" variants declare all
|
||||||
|
// gl_MaxClipDistances slots and enable only the first N, i.e. exactly the subset semantics these
|
||||||
|
// skipped cases assert. Read a green CI run here as "the state survives the frontend", not as
|
||||||
|
// "clipping is correct"; the second claim is a device claim.
|
||||||
|
//
|
||||||
|
// Every case disables all eight distances on entry rather than assuming they start off:
|
||||||
|
// XfbAfterClipDistanceScenario deliberately leaves one enabled for the rest of the process, and
|
||||||
|
// forwarding the enables is what turned that leftover from inert bookkeeping into live driver
|
||||||
|
// state.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
#ifndef GL_CLIP_DISTANCE0
|
||||||
|
#define GL_CLIP_DISTANCE0 0x3000
|
||||||
|
#endif
|
||||||
|
#ifndef GL_CLIP_DISTANCE1
|
||||||
|
#define GL_CLIP_DISTANCE1 0x3001
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// One clip distance per half of the viewport: distance 0 is positive on the right half
|
||||||
|
// (x > 0 in clip space) and distance 1 is positive on the top half. A vertex shader
|
||||||
|
// producing a full-screen triangle from gl_VertexID, so no buffers are needed.
|
||||||
|
const char* const kVertexSource = R"(#version 400 core
|
||||||
|
out float gl_ClipDistance[2];
|
||||||
|
void main() {
|
||||||
|
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||||
|
vec2 p = positions[gl_VertexID];
|
||||||
|
gl_Position = vec4(p, 0.0, 1.0);
|
||||||
|
gl_ClipDistance[0] = p.x;
|
||||||
|
gl_ClipDistance[1] = p.y;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kFragmentSource = R"(#version 400 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ClipDistanceScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
GLuint BuildProgram() {
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (!compiled) {
|
||||||
|
m_buildLog = ShaderLog(vs);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||||
|
glCompileShader(fs);
|
||||||
|
glGetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (!compiled) {
|
||||||
|
m_buildLog = ShaderLog(fs);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
if (!linked) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
m_buildLog = log.data();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& BuildLog() const { return m_buildLog; }
|
||||||
|
|
||||||
|
// Paints the whole viewport red, then draws the clipped triangle in green.
|
||||||
|
void DrawClippedTriangle(GLuint program, GLuint vao) const {
|
||||||
|
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(program);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsGreen(const unsigned char* px) {
|
||||||
|
return px[0] < 64 && px[1] > 192;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsRed(const unsigned char* px) {
|
||||||
|
return px[0] > 192 && px[1] < 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PixelAt(int x, int y, unsigned char* out) const {
|
||||||
|
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_MAX_CLIP_DISTANCES is a real backend answer, not a constant: DirectGLES reports
|
||||||
|
// 0 on a driver without GL_EXT_clip_cull_distance, and DirectVulkan reports 0 without
|
||||||
|
// the shaderClipDistance device feature. On such a stack the shader above cannot
|
||||||
|
// compile - and MUST not, because declaring a clip distance the backend cannot host
|
||||||
|
// is exactly what used to link cleanly and then render nothing. Skip rather than
|
||||||
|
// fail: there is no clipping to assert about.
|
||||||
|
static bool BackendHostsTwoClipDistances() {
|
||||||
|
GLint maxClipDistances = 0;
|
||||||
|
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||||
|
return maxClipDistances >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never assume the eight start disabled - see the header note about
|
||||||
|
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
||||||
|
static void DisableEveryClipDistance() {
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when the driver under this backend actually implements PER-DISTANCE enable
|
||||||
|
// state, i.e. when a written-but-disabled gl_ClipDistance leaves its fragments
|
||||||
|
// alone. Not every stack does, and the difference is not MobileGL's to hide:
|
||||||
|
//
|
||||||
|
// - Adreno's ES driver honours GL_CLIP_DISTANCE0_EXT..7_EXT, which is what makes
|
||||||
|
// KHR-GLxx.clip_distance.functional pass on the device once the enables are
|
||||||
|
// forwarded at all.
|
||||||
|
// - Vulkan has no such state: every clip distance a shader declares is active,
|
||||||
|
// always. DirectVulkan therefore clips by a disabled distance.
|
||||||
|
// - Mesa's llvmpipe ES driver behaves like Vulkan here.
|
||||||
|
//
|
||||||
|
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
|
||||||
|
// non-negative value inside the shader, which makes the enable mask part of the
|
||||||
|
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
|
||||||
|
// cases that need the real semantics gate on this probe and say so when they skip,
|
||||||
|
// rather than being deleted or silently weakened.
|
||||||
|
bool DriverHonoursPerDistanceEnables(GLuint program, GLuint vao) const {
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||||
|
}
|
||||||
|
DrawClippedTriangle(program, vao);
|
||||||
|
unsigned char negativeSide[4] = {0, 0, 0, 0};
|
||||||
|
glReadPixels(Gl().Width() / 4, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, negativeSide);
|
||||||
|
return IsGreen(negativeSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string ShaderLog(GLuint shader) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
return log.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The state itself: glEnable must be observable through glIsEnabled. This is the cheap half
|
||||||
|
// of the bug - SetCapability's missing case made the query answer GL_FALSE for a capability
|
||||||
|
// that had just been enabled without error.
|
||||||
|
TEST_F(ClipDistanceScenario, EnableIsObservableThroughIsEnabled) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
DisableEveryClipDistance();
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE)
|
||||||
|
<< "glDisable(GL_CLIP_DISTANCE0) is not observable through glIsEnabled";
|
||||||
|
glEnable(GL_CLIP_DISTANCE0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_TRUE)
|
||||||
|
<< "glEnable(GL_CLIP_DISTANCE0) raised no error but glIsEnabled still reports it disabled";
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_FALSE)
|
||||||
|
<< "enabling distance 0 must not enable distance 1 - the eight are independent";
|
||||||
|
|
||||||
|
glEnable(GL_CLIP_DISTANCE1);
|
||||||
|
glDisable(GL_CLIP_DISTANCE0);
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE);
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_TRUE);
|
||||||
|
|
||||||
|
glDisable(GL_CLIP_DISTANCE1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||||
|
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
ASSERT_GE(width, 8);
|
||||||
|
ASSERT_GE(height, 8);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
|
||||||
|
// Distance 1 is positive by a single pixel at the sampled row, so a stray enable on it
|
||||||
|
// would put the "kept" probe right on the clip boundary.
|
||||||
|
DisableEveryClipDistance();
|
||||||
|
glEnable(GL_CLIP_DISTANCE0);
|
||||||
|
DrawClippedTriangle(program, vao);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
unsigned char right[4] = {0, 0, 0, 0};
|
||||||
|
unsigned char left[4] = {0, 0, 0, 0};
|
||||||
|
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||||
|
PixelAt(width / 4, height / 2, left);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
EXPECT_TRUE(IsGreen(right)) << "the kept half is not painted (" << int(right[0]) << "," << int(right[1])
|
||||||
|
<< "," << int(right[2]) << ") - the draw itself did not happen, so the clipped "
|
||||||
|
"half below proves nothing";
|
||||||
|
EXPECT_TRUE(IsRed(left)) << "gl_ClipDistance[0] is negative on the left half and GL_CLIP_DISTANCE0 is "
|
||||||
|
"enabled, so those fragments must be clipped away; found ("
|
||||||
|
<< int(left[0]) << "," << int(left[1]) << "," << int(left[2]) << ")";
|
||||||
|
|
||||||
|
glDisable(GL_CLIP_DISTANCE0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The negative control: the same shader writing the same distances, with the enable off,
|
||||||
|
// must paint both halves. Without this a backend that clipped everything - or one whose
|
||||||
|
// draw simply failed - would pass the case above.
|
||||||
|
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
ASSERT_GE(width, 8);
|
||||||
|
ASSERT_GE(height, 8);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
DisableEveryClipDistance();
|
||||||
|
|
||||||
|
DrawClippedTriangle(program, vao);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
unsigned char right[4] = {0, 0, 0, 0};
|
||||||
|
unsigned char left[4] = {0, 0, 0, 0};
|
||||||
|
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||||
|
PixelAt(width / 4, height / 2, left);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
EXPECT_TRUE(IsGreen(right)) << "with every clip distance disabled the whole triangle must survive";
|
||||||
|
const bool driverHonoursEnables = IsGreen(left);
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
if (!driverHonoursEnables) {
|
||||||
|
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||||
|
<< " clips by a DISABLED gl_ClipDistance - it does not implement per-distance enable state "
|
||||||
|
"(see DriverHonoursPerDistanceEnables). Emulating GL's semantics there needs shader-side "
|
||||||
|
"masking keyed on the enable mask, which is a separate feature";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The eight enables are independent: enabling only distance 1 must clip by distance 1 and
|
||||||
|
// leave distance 0 alone. A backend that forwarded "any clip distance enabled" as a single
|
||||||
|
// bit, or that always enables every declared distance (which is what Vulkan does natively),
|
||||||
|
// passes both cases above and fails this one.
|
||||||
|
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
ASSERT_GE(width, 8);
|
||||||
|
ASSERT_GE(height, 8);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
if (!DriverHonoursPerDistanceEnables(program, vao)) {
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
DisableEveryClipDistance();
|
||||||
|
gl.EndFrame();
|
||||||
|
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||||
|
<< " clips by every declared gl_ClipDistance regardless of the enables, so per-distance "
|
||||||
|
"independence is not observable here";
|
||||||
|
}
|
||||||
|
|
||||||
|
DisableEveryClipDistance();
|
||||||
|
glEnable(GL_CLIP_DISTANCE1);
|
||||||
|
|
||||||
|
DrawClippedTriangle(program, vao);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// Distance 1 is negative on the bottom half, distance 0 on the left half. With only
|
||||||
|
// distance 1 enabled, the bottom-left must survive (distance 0 is off) and the bottom
|
||||||
|
// must not.
|
||||||
|
unsigned char topLeft[4] = {0, 0, 0, 0};
|
||||||
|
unsigned char bottomRight[4] = {0, 0, 0, 0};
|
||||||
|
PixelAt(width / 4, height - 1 - height / 4, topLeft);
|
||||||
|
PixelAt(width - 1 - width / 4, height / 4, bottomRight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
EXPECT_TRUE(IsGreen(topLeft)) << "gl_ClipDistance[0] is negative here but GL_CLIP_DISTANCE0 is disabled, so "
|
||||||
|
"this fragment must survive";
|
||||||
|
EXPECT_TRUE(IsRed(bottomRight)) << "gl_ClipDistance[1] is negative here and GL_CLIP_DISTANCE1 is enabled, so "
|
||||||
|
"this fragment must be clipped";
|
||||||
|
|
||||||
|
glDisable(GL_CLIP_DISTANCE1);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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 - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||||
|
//
|
||||||
|
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||||
|
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||||
|
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||||
|
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||||
|
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||||
|
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||||
|
//
|
||||||
|
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||||
|
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||||
|
//
|
||||||
|
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||||
|
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||||
|
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||||
|
// them and layerCount counts them; offset.z stays 0.
|
||||||
|
//
|
||||||
|
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||||
|
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||||
|
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||||
|
// scenario per pair is the coverage that matters here.
|
||||||
|
//
|
||||||
|
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||||
|
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||||
|
// slices-landed check and fail this one.
|
||||||
|
//
|
||||||
|
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||||
|
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||||
|
// slice through the same call, so the two texture kinds are read back identically.
|
||||||
|
//
|
||||||
|
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||||
|
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kWidth = 4;
|
||||||
|
constexpr int kHeight = 4;
|
||||||
|
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||||
|
// while still leaving untouched slices on either side to assert against.
|
||||||
|
constexpr int kSlices = 6;
|
||||||
|
|
||||||
|
struct Rgba8 {
|
||||||
|
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||||
|
|
||||||
|
bool operator==(const Rgba8& other) const {
|
||||||
|
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string Describe(const Rgba8& color) {
|
||||||
|
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||||
|
", " + std::to_string(color.a) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||||
|
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||||
|
// make the assertions depend on the framebuffer row order as well.
|
||||||
|
Rgba8 SourceColor(int slice) {
|
||||||
|
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||||
|
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba8 DestinationFill(int slice) {
|
||||||
|
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||||
|
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CopyImageLayeredScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!CopyImageSubDataUsable()) {
|
||||||
|
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (const GLuint texture : m_textures) {
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
m_textures.clear();
|
||||||
|
if (m_fbo != 0) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &m_fbo);
|
||||||
|
m_fbo = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||||
|
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||||
|
// simply does not have the call skips instead of failing every case below.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||||
|
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||||
|
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||||
|
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||||
|
// rather than assumed to be the image's.
|
||||||
|
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(target, texture);
|
||||||
|
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||||
|
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||||
|
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
// Fill every level, so nothing below can pass by reading a level that was never
|
||||||
|
// written and happened to hold the expected bytes.
|
||||||
|
for (int level = 0; level < levels; ++level) {
|
||||||
|
const int levelWidth = kWidth << (levels - 1 - level);
|
||||||
|
const int levelHeight = kHeight << (levels - 1 - level);
|
||||||
|
const int levelSlices =
|
||||||
|
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||||
|
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||||
|
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||||
|
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||||
|
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glBindTexture(target, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||||
|
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||||
|
// same way.
|
||||||
|
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||||
|
if (m_fbo == 0) {
|
||||||
|
glGenFramebuffers(1, &m_fbo);
|
||||||
|
}
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||||
|
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||||
|
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||||
|
// a failure - reported here rather than silently reduced to pixels[0].
|
||||||
|
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||||
|
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||||
|
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||||
|
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||||
|
}
|
||||||
|
return pixels[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||||
|
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||||
|
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||||
|
int dstZ, int depth, const char* what) {
|
||||||
|
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||||
|
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||||
|
const Rgba8 expected =
|
||||||
|
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||||
|
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||||
|
EXPECT_TRUE(actual == expected)
|
||||||
|
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||||
|
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
GLuint m_fbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||||
|
// counts carry the depth and extent.depth must stay 1.
|
||||||
|
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||||
|
kWidth, kHeight, kSlices);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||||
|
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||||
|
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||||
|
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSrcZ = 3;
|
||||||
|
constexpr int kDstZ = 1;
|
||||||
|
constexpr int kDepth = 2;
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||||
|
kDstZ, kWidth, kHeight, kDepth);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||||
|
"array->array, offset layer ranges");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||||
|
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||||
|
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSrcZ = 1;
|
||||||
|
constexpr int kDstZ = 3;
|
||||||
|
constexpr int kDepth = 3;
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||||
|
kWidth, kHeight, kDepth);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||||
|
// this is the only case where the slice count the copy may name is not the image's own -
|
||||||
|
// the bound a layered endpoint is checked against has to come from the level.
|
||||||
|
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kLevel = 1;
|
||||||
|
constexpr int kSrcZ = 2;
|
||||||
|
constexpr int kDstZ = 0;
|
||||||
|
constexpr int kDepth = 4;
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||||
|
kDstZ, kWidth, kHeight, kDepth);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||||
|
"3d->3d at mip level 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||||
|
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||||
|
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSrcZ = 2;
|
||||||
|
constexpr int kDstZ = 1;
|
||||||
|
constexpr int kDepth = 4;
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||||
|
kWidth, kHeight, kDepth);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||||
|
// while the source states it as extent.depth from a z offset.
|
||||||
|
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||||
|
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSrcZ = 1;
|
||||||
|
constexpr int kDstZ = 2;
|
||||||
|
constexpr int kDepth = 4;
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||||
|
kWidth, kHeight, kDepth);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||||
|
|
||||||
|
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.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
|
||||||
|
//
|
||||||
|
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
|
||||||
|
//
|
||||||
|
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
|
||||||
|
// nothing else, and a glCopyImageSubData naming level 1. The answer is
|
||||||
|
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
|
||||||
|
// a valid level"). MobileGL's frontend only checked the level against
|
||||||
|
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
|
||||||
|
// resolved it into a VkImageCopy subresource on a VkImage that was created with
|
||||||
|
// exactly one mip level, and the Adreno driver dereferenced the level it was
|
||||||
|
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
|
||||||
|
// mid-run. A negative case must never do that.
|
||||||
|
//
|
||||||
|
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
|
||||||
|
// rest of this file is what keeps the fix honest. A validator that answered
|
||||||
|
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
|
||||||
|
// scenarios below pin the BOUNDARY rather than the symptom:
|
||||||
|
//
|
||||||
|
// * a texture that really does have two levels must accept a copy at level 1,
|
||||||
|
// * the same texture must still reject level 2,
|
||||||
|
// * and a plain level-0 copy must move pixels, which is checked by reading the
|
||||||
|
// destination back rather than by trusting glGetError.
|
||||||
|
//
|
||||||
|
// Both backends are covered because the fix is in the shared frontend: DirectGLES
|
||||||
|
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
|
||||||
|
// not in MobileGL's, so it never reached the application either) and DirectVulkan
|
||||||
|
// records the copy itself.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr GLsizei kSize = 16;
|
||||||
|
|
||||||
|
struct Rgba8 {
|
||||||
|
GLubyte r, g, b, a;
|
||||||
|
bool operator==(const Rgba8& other) const {
|
||||||
|
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
|
||||||
|
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CopyImageLevelRangeScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DeleteTextures();
|
||||||
|
if (m_fbo != 0) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &m_fbo);
|
||||||
|
m_fbo = 0;
|
||||||
|
}
|
||||||
|
DrainErrors();
|
||||||
|
ScenarioTest::TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteTextures() {
|
||||||
|
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||||
|
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||||
|
m_src = 0;
|
||||||
|
m_dst = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One 16x16 RGBA8 texture with `levelCount` levels defined through
|
||||||
|
// glTexImage2D - the same way the CTS case builds its textures, and
|
||||||
|
// deliberately NOT glTexStorage2D: an immutable allocation would define the
|
||||||
|
// whole chain up front and could not express "level 1 does not exist".
|
||||||
|
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
for (int level = 0; level < levelCount; ++level) {
|
||||||
|
const GLsizei extent = kSize >> level;
|
||||||
|
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
pixels.data());
|
||||||
|
}
|
||||||
|
// What Utils::makeTextureComplete does in the CTS case: the texture is
|
||||||
|
// complete for the levels it actually has, not for a chain it does not.
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MakePair(int levelCount) {
|
||||||
|
DeleteTextures();
|
||||||
|
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
|
||||||
|
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The call under test, at whatever levels the caller wants, over a 1x1
|
||||||
|
// region so the region check can never be what rejects it.
|
||||||
|
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
|
||||||
|
DrainErrors();
|
||||||
|
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
|
||||||
|
extent, extent, 1);
|
||||||
|
const GLenum error = glGetError();
|
||||||
|
// A second pending error would mean the entry point queued more than one,
|
||||||
|
// and the extra would be handed out at an unrelated call site later.
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba8 ReadBackDestinationLevel0() {
|
||||||
|
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
return Rgba8{0, 0, 0, 0};
|
||||||
|
}
|
||||||
|
Rgba8 texel{0, 0, 0, 0};
|
||||||
|
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
return texel;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_src = 0;
|
||||||
|
GLuint m_dst = 0;
|
||||||
|
GLuint m_fbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The regression. Level 1 of a texture that has only level 0 is not a level, and
|
||||||
|
// saying so is the whole job: before the fix this reached DirectVulkan, which
|
||||||
|
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
|
||||||
|
// Adreno driver.
|
||||||
|
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
MakePair(1);
|
||||||
|
|
||||||
|
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
|
||||||
|
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
|
||||||
|
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The negative control that makes the test above falsifiable: the same level
|
||||||
|
// index, on textures that genuinely have it, must be accepted. A validator that
|
||||||
|
// rejected every non-zero level would pass the regression test and fail here.
|
||||||
|
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
MakePair(2);
|
||||||
|
|
||||||
|
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the boundary from the other side: two levels means 0 and 1, not 2.
|
||||||
|
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
MakePair(2);
|
||||||
|
|
||||||
|
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
|
||||||
|
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
|
||||||
|
// ordinary case is checked by reading the destination back: the copy has to move
|
||||||
|
// the source's texel, not merely decline to complain.
|
||||||
|
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
MakePair(1);
|
||||||
|
|
||||||
|
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
|
||||||
|
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
+370
@@ -0,0 +1,370 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackAttachmentShapeScenario.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 - DEPTH/STENCIL READBACK WHEN THE ATTACHMENT IS NOT A PLAIN GL_TEXTURE_2D,
|
||||||
|
// AND THE DEFAULT FRAMEBUFFER'S ADVERTISED DEPTH/STENCIL FORMAT.
|
||||||
|
//
|
||||||
|
// Three shipped defects, all of them invisible to a test that only ever attaches a 2D texture
|
||||||
|
// or only ever asks the default framebuffer for a colour value.
|
||||||
|
//
|
||||||
|
// (1) The ES depth/stencil readback emulation identifies the source format by binding the
|
||||||
|
// attachment's texture NAME to GL_TEXTURE_2D and asking that target for its internal
|
||||||
|
// format. A name whose target is GL_TEXTURE_2D_ARRAY (attached by
|
||||||
|
// glFramebufferTextureLayer) makes the bind answer GL_INVALID_OPERATION and change
|
||||||
|
// nothing - so the query then truthfully describes whatever texture was already on
|
||||||
|
// GL_TEXTURE_2D, which on that path is the emulation's own staging scratch. A wrong
|
||||||
|
// answer that looks like a right one: the staging blit is issued between mismatched
|
||||||
|
// depth formats, ES rejects it, and the read reports nothing at all.
|
||||||
|
//
|
||||||
|
// (2) Adreno answers GL_NONE for GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE on an attachment made
|
||||||
|
// by glFramebufferTexture (a cube map, attached layered) while still reporting its depth
|
||||||
|
// and stencil bits correctly. The emulation took OBJECT_TYPE as the sole witness for "is
|
||||||
|
// there an aspect here at all" and declined the whole read.
|
||||||
|
//
|
||||||
|
// (3) DirectGLES never told the frontend what its default framebuffer's depth/stencil format
|
||||||
|
// actually is, so the placeholder from MG_Impl/Init.cpp - GL_DEPTH32F_STENCIL8 - was what
|
||||||
|
// every attachment query answered, whatever the surface really had. That is not cosmetic:
|
||||||
|
// GL blits depth/stencil only between IDENTICAL formats, so an application that reads
|
||||||
|
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was just told about and
|
||||||
|
// blits gets GL_INVALID_OPERATION - and a rejected glBlitFramebuffer transfers NOTHING,
|
||||||
|
// colour bits included. DirectVulkan has published its real format since the swapchain
|
||||||
|
// work; this is the half that was missing.
|
||||||
|
//
|
||||||
|
// Every case poisons its destination with a value the correct answer cannot be, so "the
|
||||||
|
// backend wrote nothing" fails loudly instead of passing on stale memory. The plain
|
||||||
|
// GL_TEXTURE_2D case at the end is the built-in control: it shares every line of the readback
|
||||||
|
// path with the array and cube cases, so its passing is what says a failure above is about the
|
||||||
|
// attachment's SHAPE and not about depth readback in general.
|
||||||
|
//
|
||||||
|
// The scenario name starts with DepthStencilReadback on purpose - that is the filter the
|
||||||
|
// forced-emulation ctest registration uses (MG_IntegrationTest/CMakeLists.txt), and without
|
||||||
|
// that registration these cases are unfalsifiable on llvmpipe, which accepts the native ES
|
||||||
|
// depth reads that the Adreno device does not have.
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kDepthPoison = 0.2f;
|
||||||
|
constexpr int kStencilPoison = 50;
|
||||||
|
constexpr float kDepthValue = 0.75f;
|
||||||
|
constexpr int kStencilValue = 7;
|
||||||
|
constexpr int kSize = 16;
|
||||||
|
|
||||||
|
class DepthStencilReadbackAttachmentShapeScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
float ReadDepthAt(int x, int y) const {
|
||||||
|
float depth = kDepthPoison;
|
||||||
|
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReadStencilAt(int x, int y) const {
|
||||||
|
int stencil = kStencilPoison;
|
||||||
|
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||||
|
return stencil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears the currently bound framebuffer's depth and stencil to the shared
|
||||||
|
// reference values, with both write masks explicitly open (glClear honours them,
|
||||||
|
// and a leftover mask from another scenario in this shared context would look
|
||||||
|
// exactly like the bug under test).
|
||||||
|
void ClearDepthStencil() const {
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glStencilMask(0xFFu);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glClearDepth(kDepthValue);
|
||||||
|
glClearStencil(kStencilValue);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fails the calling test if the framebuffer bound at both targets is not complete;
|
||||||
|
// an incomplete framebuffer would make every read below return the poison for a
|
||||||
|
// reason that has nothing to do with what is being tested.
|
||||||
|
::testing::AssertionResult FramebufferIsComplete() {
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||||
|
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// (1) A depth slice of a 2D ARRAY texture, attached with glFramebufferTextureLayer.
|
||||||
|
// Pre-fix this read back the poison: the format probe answered with the staging scratch's
|
||||||
|
// GL_DEPTH24_STENCIL8 instead of the array's GL_DEPTH_COMPONENT24, and the mismatched
|
||||||
|
// staging blit was rejected.
|
||||||
|
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAnArrayLayerAttachmentReadsBack) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint depthArray = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenTextures(1, &depthArray);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, depthArray);
|
||||||
|
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kSize, kSize, 4);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
// Layer 2, not layer 0: a backend that silently reads the wrong slice would still
|
||||||
|
// agree with a single-layer texture.
|
||||||
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, 2);
|
||||||
|
glDrawBuffer(GL_NONE);
|
||||||
|
glReadBuffer(GL_NONE);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_TRUE(FramebufferIsComplete());
|
||||||
|
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
ClearDepthStencil();
|
||||||
|
|
||||||
|
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||||
|
<< "glReadPixels(GL_DEPTH_COMPONENT) of a GL_TEXTURE_2D_ARRAY layer attachment returned " << depth
|
||||||
|
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &depthArray);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) A depth cube map, attached whole with glFramebufferTexture - a LAYERED attachment.
|
||||||
|
// Pre-fix the emulation declined outright, because the driver reports GL_NONE for that
|
||||||
|
// attachment's OBJECT_TYPE.
|
||||||
|
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfALayeredCubeAttachmentReadsBack) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint depthCube = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenTextures(1, &depthCube);
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCube);
|
||||||
|
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCube, 0);
|
||||||
|
glDrawBuffer(GL_NONE);
|
||||||
|
glReadBuffer(GL_NONE);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_TRUE(FramebufferIsComplete());
|
||||||
|
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
ClearDepthStencil();
|
||||||
|
|
||||||
|
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||||
|
<< "glReadPixels(GL_DEPTH_COMPONENT) of a layered GL_TEXTURE_CUBE_MAP attachment returned " << depth
|
||||||
|
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &depthCube);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both aspects of a packed array attachment. The stencil half goes through a different
|
||||||
|
// sampling mode than the depth half, and only the depth half was covered above.
|
||||||
|
TEST_F(DepthStencilReadbackAttachmentShapeScenario, PackedArrayLayerAttachmentReadsBackBothAspects) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint packedArray = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenTextures(1, &packedArray);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, packedArray);
|
||||||
|
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, kSize, kSize, 3);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, packedArray, 0, 1);
|
||||||
|
glDrawBuffer(GL_NONE);
|
||||||
|
glReadBuffer(GL_NONE);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_TRUE(FramebufferIsComplete());
|
||||||
|
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
ClearDepthStencil();
|
||||||
|
|
||||||
|
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||||
|
const int stencil = ReadStencilAt(kSize / 2, kSize / 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||||
|
<< "depth of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << depth;
|
||||||
|
EXPECT_EQ(stencil, kStencilValue)
|
||||||
|
<< "stencil of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << stencil
|
||||||
|
<< (stencil == kStencilPoison ? " - the destination was never written at all" : "");
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &packedArray);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control: the plain GL_TEXTURE_2D shape, which always worked. If this one ever fails
|
||||||
|
// alongside the three above, the fault is in depth readback generally rather than in how
|
||||||
|
// the attachment's format and presence are discovered.
|
||||||
|
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAPlainTexture2DAttachmentReadsBack) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint depthTex = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenTextures(1, &depthTex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||||
|
glDrawBuffer(GL_NONE);
|
||||||
|
glReadBuffer(GL_NONE);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_TRUE(FramebufferIsComplete());
|
||||||
|
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
ClearDepthStencil();
|
||||||
|
|
||||||
|
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||||
|
<< "the control case failed: even a plain GL_TEXTURE_2D depth attachment read back " << depth;
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &depthTex);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) The default framebuffer must describe its depth/stencil truthfully enough that a
|
||||||
|
// buffer allocated from that description is blit-compatible with it. This is the exact
|
||||||
|
// sequence KHR-GLxx.framebuffer_blit performs, and the exact reason 22 of its cases died
|
||||||
|
// on DirectGLES: the frontend answered 32-bit float depth for a 24-bit fixed-point
|
||||||
|
// surface, so the renderbuffer the caller allocated could never be blitted to.
|
||||||
|
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DefaultFramebufferDepthStencilFormatIsBlitCompatible) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
GLint depthBits = 0;
|
||||||
|
GLint stencilBits = 0;
|
||||||
|
GLint componentType = GL_UNSIGNED_NORMALIZED;
|
||||||
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||||
|
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
||||||
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
|
||||||
|
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
||||||
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||||
|
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
if (depthBits <= 0 || stencilBits <= 0) {
|
||||||
|
GTEST_SKIP() << "this surface has no packed depth/stencil (depth=" << depthBits
|
||||||
|
<< " stencil=" << stencilBits << "); the blit-compatibility contract needs both";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one sized format the reported description names. Getting here with the wrong
|
||||||
|
// answer is the bug: the two candidates are not interchangeable for a blit.
|
||||||
|
const GLenum reported = (componentType == GL_FLOAT || depthBits > 24) ? GL_DEPTH32F_STENCIL8
|
||||||
|
: GL_DEPTH24_STENCIL8;
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint colorRbo = 0;
|
||||||
|
GLuint depthRbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenRenderbuffers(1, &colorRbo);
|
||||||
|
glGenRenderbuffers(1, &depthRbo);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, colorRbo);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, reported, width, height);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_TRUE(FramebufferIsComplete());
|
||||||
|
|
||||||
|
// Put a known depth in the default framebuffer, then blit colour+depth+stencil out of
|
||||||
|
// it into the buffer that its own description asked for.
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
ClearDepthStencil();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||||
|
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
|
||||||
|
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u)
|
||||||
|
<< "blitting depth/stencil out of the default framebuffer into a buffer allocated from the format "
|
||||||
|
"the default framebuffer itself reported was rejected - the report and the storage disagree";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||||
|
unsigned char color[4] = {0, 0, 0, 0};
|
||||||
|
glReadPixels(width / 2, height / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
|
||||||
|
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||||
|
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
// The colour bit is the precondition, not the claim: it says this stack can blit out of
|
||||||
|
// its default framebuffer at all, which has nothing to do with depth/stencil formats.
|
||||||
|
// DirectVulkan on a surfaceless pbuffer cannot - the whole call, colour included, is a
|
||||||
|
// no-op there, while the same blit works on a real surface (KHR-GLxx.framebuffer_blit
|
||||||
|
// exercises exactly it and Magma passes 33/33 on device). Skipping keeps the
|
||||||
|
// depth/stencil claim below falsifiable instead of drowning it in an unrelated
|
||||||
|
// harness limitation.
|
||||||
|
if (int(color[1]) <= 192) {
|
||||||
|
// GTEST_SKIP() expands to a return, so the teardown below it would never run and this
|
||||||
|
// scenario would hand the next one a foreign framebuffer plus three leaked objects -
|
||||||
|
// and this is the path DirectVulkan takes on every headless run, not a rare one.
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &colorRbo);
|
||||||
|
glDeleteRenderbuffers(1, &depthRbo);
|
||||||
|
gl.EndFrame();
|
||||||
|
GTEST_SKIP() << "backend " << gl.BackendName() << " on this surface transferred no colour either (green="
|
||||||
|
<< int(color[1])
|
||||||
|
<< "): it cannot blit out of the default framebuffer here, so the depth/stencil half proves "
|
||||||
|
"nothing. The GL-error assertion above still ran, and it is the format contract";
|
||||||
|
}
|
||||||
|
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||||
|
<< "depth blitted out of the default framebuffer read back " << depth
|
||||||
|
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the blit transferred nothing" : "");
|
||||||
|
EXPECT_EQ(stencil, kStencilValue) << "stencil blitted out of the default framebuffer read back " << stencil;
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &colorRbo);
|
||||||
|
glDeleteRenderbuffers(1, &depthRbo);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,784 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE DEPTH/STENCIL READBACK MATRIX: every verb, every source kind.
|
||||||
|
//
|
||||||
|
// DepthStencilReadbackScenario pins the default framebuffer. This file pins the rest of
|
||||||
|
// the surface a depth/stencil read has to cover, because the three verbs and the four
|
||||||
|
// source kinds do NOT share a code path by accident - they share one on purpose, and a
|
||||||
|
// change that quietly serves only one of them is exactly what these assertions catch:
|
||||||
|
//
|
||||||
|
// verbs glReadPixels(GL_DEPTH_COMPONENT | GL_STENCIL_INDEX | GL_DEPTH_STENCIL),
|
||||||
|
// glGetTexImage(GL_DEPTH_STENCIL), glCopyTexImage2D followed by a read
|
||||||
|
// source kinds depth(-stencil) TEXTURE, RENDERBUFFER (not samplable at all),
|
||||||
|
// MULTISAMPLE renderbuffer (needs a resolve first), default framebuffer
|
||||||
|
// formats DEPTH24_STENCIL8, DEPTH32F_STENCIL8, DEPTH_COMPONENT16/24/32F,
|
||||||
|
// STENCIL_INDEX8
|
||||||
|
// client types GL_FLOAT / GL_UNSIGNED_INT / GL_UNSIGNED_SHORT depth, GL_INT /
|
||||||
|
// GL_UNSIGNED_BYTE stencil, both packed GL_DEPTH_STENCIL layouts
|
||||||
|
//
|
||||||
|
// On DirectGLES none of this exists natively - ES has no depth or stencil readback in
|
||||||
|
// core - so every assertion here is really an assertion about the shader-sampling
|
||||||
|
// emulation. The catch is that some ES drivers accept the reads anyway (Mesa does,
|
||||||
|
// Adreno does not), which would make the emulation dead code on the very stack the
|
||||||
|
// headless suite runs on. That is what the second ctest registration is for: the same
|
||||||
|
// scenarios run again with MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1, which takes the
|
||||||
|
// native spellings off the table and leaves only the path the device actually uses.
|
||||||
|
//
|
||||||
|
// Every destination is poisoned with a value the correct answer cannot be, so "the
|
||||||
|
// backend wrote nothing" fails loudly instead of passing on a coincidence - a test that
|
||||||
|
// only checked "no GL error" would pass against a readback that never touched the buffer,
|
||||||
|
// which is precisely how this whole cluster hid for so long.
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kDepthPoison = 0.2f;
|
||||||
|
constexpr int kStencilPoison = 50;
|
||||||
|
constexpr int kWidth = 64;
|
||||||
|
constexpr int kHeight = 48;
|
||||||
|
|
||||||
|
// A depth-stencil pair no clear in these tests produces, packed both ways.
|
||||||
|
constexpr unsigned int kPacked24_8Poison = 0xAAAAAA33u;
|
||||||
|
|
||||||
|
struct D32fS8 {
|
||||||
|
float depth;
|
||||||
|
unsigned int stencil;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything a source needs to be read: the framebuffer to bind, plus the objects
|
||||||
|
// to delete afterwards.
|
||||||
|
struct DepthSource {
|
||||||
|
GLuint fbo = 0;
|
||||||
|
GLuint colorTexture = 0;
|
||||||
|
GLuint depthTexture = 0;
|
||||||
|
GLuint depthRenderbuffer = 0;
|
||||||
|
GLuint colorRenderbuffer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void DestroySource(DepthSource& source) {
|
||||||
|
if (source.fbo != 0) glDeleteFramebuffers(1, &source.fbo);
|
||||||
|
if (source.colorTexture != 0) glDeleteTextures(1, &source.colorTexture);
|
||||||
|
if (source.depthTexture != 0) glDeleteTextures(1, &source.depthTexture);
|
||||||
|
if (source.depthRenderbuffer != 0) glDeleteRenderbuffers(1, &source.depthRenderbuffer);
|
||||||
|
if (source.colorRenderbuffer != 0) glDeleteRenderbuffers(1, &source.colorRenderbuffer);
|
||||||
|
source = DepthSource{};
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum AttachmentPointFor(GLenum internalFormat) {
|
||||||
|
switch (internalFormat) {
|
||||||
|
case GL_DEPTH24_STENCIL8:
|
||||||
|
case GL_DEPTH32F_STENCIL8: return GL_DEPTH_STENCIL_ATTACHMENT;
|
||||||
|
case GL_STENCIL_INDEX8: return GL_STENCIL_ATTACHMENT;
|
||||||
|
default: return GL_DEPTH_ATTACHMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FormatHasDepth(GLenum internalFormat) { return internalFormat != GL_STENCIL_INDEX8; }
|
||||||
|
bool FormatHasStencil(GLenum internalFormat) {
|
||||||
|
return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 ||
|
||||||
|
internalFormat == GL_STENCIL_INDEX8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A framebuffer whose depth/stencil lives in a TEXTURE. The colour attachment is
|
||||||
|
// there so a stencil-only or depth-only framebuffer still has something to size it.
|
||||||
|
DepthSource MakeTextureSource(GLenum internalFormat) {
|
||||||
|
DepthSource source;
|
||||||
|
glGenFramebuffers(1, &source.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||||
|
glGenTextures(1, &source.colorTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, source.colorTexture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.colorTexture, 0);
|
||||||
|
glGenTextures(1, &source.depthTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_TEXTURE_2D,
|
||||||
|
source.depthTexture, 0);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same, with the depth/stencil in a RENDERBUFFER - which cannot be sampled at
|
||||||
|
// all, so the readback has no choice but to copy it somewhere samplable first.
|
||||||
|
// `samples` > 0 makes it multisample, which additionally needs a resolve.
|
||||||
|
DepthSource MakeRenderbufferSource(GLenum internalFormat, int samples) {
|
||||||
|
DepthSource source;
|
||||||
|
glGenFramebuffers(1, &source.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||||
|
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||||
|
if (samples > 0) {
|
||||||
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kWidth, kHeight);
|
||||||
|
} else {
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||||
|
}
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||||
|
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||||
|
if (samples > 0) {
|
||||||
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, kWidth, kHeight);
|
||||||
|
} else {
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
|
||||||
|
}
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_RENDERBUFFER,
|
||||||
|
source.depthRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears the bound framebuffer's depth and stencil to known values, with the masks
|
||||||
|
// and the scissor explicitly out of the way (a leaked scissor from an earlier
|
||||||
|
// scenario would clip the clear and every assertion after it).
|
||||||
|
void ClearDepthStencil(GLenum internalFormat, float depth, int stencil) {
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
GLbitfield mask = 0;
|
||||||
|
if (FormatHasDepth(internalFormat)) {
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glClearDepth(depth);
|
||||||
|
mask |= GL_DEPTH_BUFFER_BIT;
|
||||||
|
}
|
||||||
|
if (FormatHasStencil(internalFormat)) {
|
||||||
|
glStencilMask(0xFFu);
|
||||||
|
glClearStencil(stencil);
|
||||||
|
mask |= GL_STENCIL_BUFFER_BIT;
|
||||||
|
}
|
||||||
|
glClear(mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DepthStencilReadbackMatrixScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
// Not every ES driver can render to every depth format (DEPTH_COMPONENT32F and
|
||||||
|
// the multisample counts in particular), and an incomplete framebuffer would
|
||||||
|
// turn a legitimate "this machine cannot host the source" into a spurious
|
||||||
|
// failure about the readback.
|
||||||
|
static bool SourceIsUsable() {
|
||||||
|
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GLenum(GL_FRAMEBUFFER_COMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<float> ReadDepthFloat(int x, int y, int width, int height) {
|
||||||
|
std::vector<float> depth(static_cast<size_t>(width) * height, kDepthPoison);
|
||||||
|
glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data());
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<int> ReadStencilInt(int x, int y, int width, int height) {
|
||||||
|
std::vector<int> stencil(static_cast<size_t>(width) * height, kStencilPoison);
|
||||||
|
glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_INT, stencil.data());
|
||||||
|
return stencil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "every value in the region is `expected`" rather than "the middle pixel is":
|
||||||
|
// a staging blit that lands the wrong rectangle, or a conversion pass with a
|
||||||
|
// half-texel offset, still gets the centre right.
|
||||||
|
static void ExpectAllDepth(const std::vector<float>& values, float expected, const char* what) {
|
||||||
|
size_t bad = 0;
|
||||||
|
float worst = expected;
|
||||||
|
for (float value : values) {
|
||||||
|
if (std::fabs(value - expected) > 1.0f / 4096.0f) {
|
||||||
|
if (bad == 0) worst = value;
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||||
|
<< " depth values differ from " << expected << "; first bad value " << worst
|
||||||
|
<< (std::fabs(worst - kDepthPoison) < 1e-6f
|
||||||
|
? " - which is the poison value, so nothing was written at all"
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExpectAllStencil(const std::vector<int>& values, int expected, const char* what) {
|
||||||
|
size_t bad = 0;
|
||||||
|
int worst = expected;
|
||||||
|
for (int value : values) {
|
||||||
|
if (value != expected) {
|
||||||
|
if (bad == 0) worst = value;
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||||
|
<< " stencil values differ from " << expected << "; first bad value " << worst
|
||||||
|
<< (worst == kStencilPoison
|
||||||
|
? " - which is the poison value, so nothing was written at all"
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- glReadPixels across the source kinds -----------------------------------
|
||||||
|
|
||||||
|
struct SourceCase {
|
||||||
|
const char* name;
|
||||||
|
GLenum internalFormat;
|
||||||
|
int samples;
|
||||||
|
bool renderbuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SourceCase kSourceCases[] = {
|
||||||
|
{"texture depth24_stencil8", GL_DEPTH24_STENCIL8, 0, false},
|
||||||
|
{"texture depth32f_stencil8", GL_DEPTH32F_STENCIL8, 0, false},
|
||||||
|
{"texture depth_component16", GL_DEPTH_COMPONENT16, 0, false},
|
||||||
|
{"texture depth_component24", GL_DEPTH_COMPONENT24, 0, false},
|
||||||
|
{"texture depth_component32f", GL_DEPTH_COMPONENT32F, 0, false},
|
||||||
|
{"renderbuffer depth24_stencil8", GL_DEPTH24_STENCIL8, 0, true},
|
||||||
|
{"renderbuffer depth_component24", GL_DEPTH_COMPONENT24, 0, true},
|
||||||
|
{"renderbuffer stencil_index8", GL_STENCIL_INDEX8, 0, true},
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, EverySourceKindReadsItsClearBack) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
int exercised = 0;
|
||||||
|
for (const SourceCase& testCase : kSourceCases) {
|
||||||
|
SCOPED_TRACE(testCase.name);
|
||||||
|
DepthSource source = testCase.renderbuffer
|
||||||
|
? MakeRenderbufferSource(testCase.internalFormat, testCase.samples)
|
||||||
|
: MakeTextureSource(testCase.internalFormat);
|
||||||
|
if (!SourceIsUsable()) {
|
||||||
|
DestroySource(source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FirstGLError(); // the storage calls above may have probed an unsupported combination
|
||||||
|
ClearDepthStencil(testCase.internalFormat, 0.625f, 9);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "clearing the source";
|
||||||
|
|
||||||
|
if (FormatHasDepth(testCase.internalFormat)) {
|
||||||
|
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_FLOAT)";
|
||||||
|
ExpectAllDepth(depth, 0.625f, testCase.name);
|
||||||
|
}
|
||||||
|
if (FormatHasStencil(testCase.internalFormat)) {
|
||||||
|
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||||
|
ExpectAllStencil(stencil, 9, testCase.name);
|
||||||
|
}
|
||||||
|
++exercised;
|
||||||
|
DestroySource(source);
|
||||||
|
}
|
||||||
|
// A machine that hosted none of the sources would report a vacuous pass.
|
||||||
|
EXPECT_GE(exercised, 4) << "too few depth/stencil source kinds were usable to call this a matrix";
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depth and stencil in two SEPARATE objects, with two different formats, on the same
|
||||||
|
// framebuffer. Legal GL, and the shape KHR-GL3x.framebuffer_blit builds when its depth
|
||||||
|
// config and its stencil config are configured independently - so a readback that
|
||||||
|
// describes "the" depth/stencil source as one thing serves whichever aspect it happened
|
||||||
|
// to find first and silently abandons the other. Each aspect has to be staged from its
|
||||||
|
// own attachment, in its own format.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, SeparateDepthAndStencilAttachmentsAreBothReadable) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source;
|
||||||
|
glGenFramebuffers(1, &source.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||||
|
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||||
|
// Depth in a DEPTH_COMPONENT24 renderbuffer...
|
||||||
|
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kWidth, kHeight);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||||
|
// ...and stencil in a STENCIL_INDEX8 one of its own.
|
||||||
|
GLuint stencilRenderbuffer = 0;
|
||||||
|
glGenRenderbuffers(1, &stencilRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, stencilRenderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, kWidth, kHeight);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilRenderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
if (!SourceIsUsable()) {
|
||||||
|
// Separate depth and stencil images are legal GL but many stacks answer
|
||||||
|
// GL_FRAMEBUFFER_UNSUPPORTED for them; say which, so a skip here is a fact about
|
||||||
|
// the driver rather than an unexplained hole in the matrix.
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||||
|
DestroySource(source);
|
||||||
|
GTEST_SKIP() << "this driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments: "
|
||||||
|
<< "glCheckFramebufferStatus = 0x" << std::hex << status;
|
||||||
|
}
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glStencilMask(0xFFu);
|
||||||
|
glClearDepth(0.3125);
|
||||||
|
glClearStencil(17);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "reading depth from a separately-attached DEPTH_COMPONENT24";
|
||||||
|
ExpectAllDepth(depth, 0.3125f, "separate depth attachment");
|
||||||
|
|
||||||
|
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "reading stencil from a separately-attached STENCIL_INDEX8";
|
||||||
|
ExpectAllStencil(stencil, 17, "separate stencil attachment");
|
||||||
|
|
||||||
|
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||||
|
DestroySource(source);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A multisample source is never read directly - glReadPixels on a multisampled
|
||||||
|
// framebuffer is INVALID_OPERATION in GL as much as in ES, and the state layer says so.
|
||||||
|
// The way multisample depth reaches a reader is a resolve blit into a single-sampled
|
||||||
|
// framebuffer, which is then read; that pair is
|
||||||
|
// KHR-GL3x.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test, and
|
||||||
|
// the assertion here is that the resolved depth arrives intact rather than as the
|
||||||
|
// destination's own clear value.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, AResolvedMultisampleDepthReadsBackFromTheDestination) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4);
|
||||||
|
if (!SourceIsUsable()) {
|
||||||
|
DestroySource(multisampled);
|
||||||
|
GTEST_SKIP() << "this driver cannot host a 4x multisample DEPTH24_STENCIL8 renderbuffer";
|
||||||
|
}
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.875f, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// The destination starts at a depth the resolve must overwrite everywhere.
|
||||||
|
DepthSource resolved = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.125f, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampled.fbo);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "resolving a multisample depth buffer into a single-sampled one";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, resolved.fbo);
|
||||||
|
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectAllDepth(depth, 0.875f, "resolved multisample depth");
|
||||||
|
|
||||||
|
DestroySource(resolved);
|
||||||
|
DestroySource(multisampled);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A read whose rectangle is NOT the whole attachment. The staging copy has to carry
|
||||||
|
// the requested rect (not the origin) and hand back its rows bottom-up, which a
|
||||||
|
// full-extent uniform read is a fixed point of and therefore cannot see.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, ASubRectangleReadsTheRightBandInTheRightOrder) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
|
||||||
|
// Bottom half 0.25, top half 0.75, and the stencil banded the other way round so a
|
||||||
|
// mix-up between the two aspects cannot pass either.
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glStencilMask(0xFFu);
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(0, 0, kWidth, kHeight / 2);
|
||||||
|
glClearDepth(0.25);
|
||||||
|
glClearStencil(11);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
glScissor(0, kHeight / 2, kWidth, kHeight - kHeight / 2);
|
||||||
|
glClearDepth(0.75);
|
||||||
|
glClearStencil(22);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// A rect wholly inside the bottom band, offset from the origin in both axes.
|
||||||
|
const int rectWidth = 8;
|
||||||
|
const int rectHeight = 4;
|
||||||
|
const std::vector<float> bottom = ReadDepthFloat(16, 4, rectWidth, rectHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectAllDepth(bottom, 0.25f, "sub-rect inside the bottom depth band");
|
||||||
|
const std::vector<int> bottomStencil = ReadStencilInt(16, 4, rectWidth, rectHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectAllStencil(bottomStencil, 11, "sub-rect inside the bottom stencil band");
|
||||||
|
|
||||||
|
// And one wholly inside the top band. Reading the mirrored row would answer 0.25.
|
||||||
|
const std::vector<float> top = ReadDepthFloat(16, kHeight - 4 - rectHeight, rectWidth, rectHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectAllDepth(top, 0.75f, "sub-rect inside the top depth band");
|
||||||
|
|
||||||
|
// A rect that STRADDLES the boundary pins the row order itself: its first rows must
|
||||||
|
// be the bottom band and its last rows the top one.
|
||||||
|
const int straddleHeight = 8;
|
||||||
|
const std::vector<float> straddle =
|
||||||
|
ReadDepthFloat(16, kHeight / 2 - straddleHeight / 2, rectWidth, straddleHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_EQ(straddle.size(), static_cast<size_t>(rectWidth) * straddleHeight);
|
||||||
|
EXPECT_NEAR(straddle[0], 0.25f, 1.0f / 4096.0f)
|
||||||
|
<< "the first row of the returned rect must be its BOTTOM row (GL order), which is in the 0.25 band";
|
||||||
|
EXPECT_NEAR(straddle[straddle.size() - 1], 0.75f, 1.0f / 4096.0f)
|
||||||
|
<< "the last row of the returned rect must be its TOP row, which is in the 0.75 band";
|
||||||
|
|
||||||
|
DestroySource(source);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The packed layouts the packed_depth_stencil family reads its gradients with.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, PackedDepthStencilReadPixelsCarriesBothAspects) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
struct PackedCase {
|
||||||
|
const char* name;
|
||||||
|
GLenum internalFormat;
|
||||||
|
GLenum type;
|
||||||
|
};
|
||||||
|
const PackedCase cases[] = {
|
||||||
|
{"depth24_stencil8 / GL_UNSIGNED_INT_24_8", GL_DEPTH24_STENCIL8, GL_UNSIGNED_INT_24_8},
|
||||||
|
{"depth32f_stencil8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV", GL_DEPTH32F_STENCIL8,
|
||||||
|
GL_FLOAT_32_UNSIGNED_INT_24_8_REV},
|
||||||
|
};
|
||||||
|
int exercised = 0;
|
||||||
|
for (const PackedCase& testCase : cases) {
|
||||||
|
SCOPED_TRACE(testCase.name);
|
||||||
|
DepthSource source = MakeTextureSource(testCase.internalFormat);
|
||||||
|
if (!SourceIsUsable()) {
|
||||||
|
DestroySource(source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(testCase.internalFormat, 0.5f, 3);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||||
|
if (testCase.type == GL_UNSIGNED_INT_24_8) {
|
||||||
|
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
size_t bad = 0;
|
||||||
|
for (unsigned int value : packed) {
|
||||||
|
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||||
|
const int stencil = static_cast<int>(value & 0xFFu);
|
||||||
|
if (std::fabs(depth - 0.5f) > 0.01f || stencil != 3) ++bad;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||||
|
<< " packed words carry the wrong depth or stencil (first word 0x" << std::hex
|
||||||
|
<< packed[0] << std::dec << ")";
|
||||||
|
} else {
|
||||||
|
std::vector<D32fS8> packed(pixels, D32fS8{kDepthPoison, static_cast<unsigned int>(kStencilPoison)});
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
size_t bad = 0;
|
||||||
|
for (const D32fS8& value : packed) {
|
||||||
|
if (std::fabs(value.depth - 0.5f) > 0.01f || (value.stencil & 0xFFu) != 3u) ++bad;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||||
|
<< " packed pairs carry the wrong depth or stencil (first pair depth "
|
||||||
|
<< packed[0].depth << " stencil " << (packed[0].stencil & 0xFFu) << ")";
|
||||||
|
}
|
||||||
|
++exercised;
|
||||||
|
DestroySource(source);
|
||||||
|
}
|
||||||
|
EXPECT_GE(exercised, 1) << "neither packed depth/stencil format was renderable";
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// glGetTexImage reads a TEXTURE, not the bound framebuffer - a different entry point
|
||||||
|
// that has to reach the same machinery. This is verify_get_tex_image's shape.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, GetTexImageReadsAPackedDepthStencilTexture) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.375f, 5);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// Read it back through the texture, with the framebuffer that owns it unbound so a
|
||||||
|
// path that secretly read the framebuffer instead would answer from somewhere else.
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||||
|
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||||
|
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
size_t bad = 0;
|
||||||
|
for (unsigned int value : packed) {
|
||||||
|
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||||
|
if (std::fabs(depth - 0.375f) > 0.01f || (value & 0xFFu) != 5u) ++bad;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << bad << " of " << pixels
|
||||||
|
<< " words from glGetTexImage(GL_DEPTH_STENCIL) are wrong (first word 0x" << std::hex
|
||||||
|
<< packed[0] << std::dec << ")";
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
DestroySource(source);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// glCopyTexImage2D out of a depth attachment, then read the copy - verify_copy_tex_image.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, CopyTexImageFromADepthAttachmentSurvivesAReadBack) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.75f, 6);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
GLuint copy = 0;
|
||||||
|
glGenTextures(1, ©);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, copy);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, kWidth, kHeight, 0, GL_DEPTH_STENCIL,
|
||||||
|
GL_UNSIGNED_INT_24_8, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 0, 0, kWidth, kHeight, 0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glCopyTexImage2D from a depth/stencil attachment";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||||
|
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
size_t bad = 0;
|
||||||
|
for (unsigned int value : packed) {
|
||||||
|
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||||
|
if (std::fabs(depth - 0.75f) > 0.01f) ++bad;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(bad, 0u) << bad << " of " << pixels << " copied depth values are wrong (first word 0x" << std::hex
|
||||||
|
<< packed[0] << std::dec << ")";
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, ©);
|
||||||
|
DestroySource(source);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The integer client widths, which are a separate conversion each.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, DepthAndStencilConvertIntoEveryClientWidth) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 200);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||||
|
|
||||||
|
std::vector<unsigned int> depthUint(pixels, 0xDEADBEEFu);
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, depthUint.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_INT)";
|
||||||
|
// 0.5 of the full 32-bit range, with room for the source's 24-bit quantisation.
|
||||||
|
EXPECT_NEAR(static_cast<double>(depthUint[0]) / 4294967295.0, 0.5, 0.01)
|
||||||
|
<< "GL_UNSIGNED_INT depth came back as " << depthUint[0];
|
||||||
|
|
||||||
|
std::vector<unsigned short> depthUshort(pixels, 0xBEEFu);
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, depthUshort.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT)";
|
||||||
|
EXPECT_NEAR(static_cast<double>(depthUshort[0]) / 65535.0, 0.5, 0.01)
|
||||||
|
<< "GL_UNSIGNED_SHORT depth came back as " << depthUshort[0];
|
||||||
|
|
||||||
|
// A stencil index is written unconverted into whichever width was asked for, so 200
|
||||||
|
// must survive intact in all of them - it is also large enough that a signed byte
|
||||||
|
// would wrap, which is the point of choosing it.
|
||||||
|
std::vector<unsigned char> stencilByte(pixels, static_cast<unsigned char>(kStencilPoison));
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilByte.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE)";
|
||||||
|
EXPECT_EQ(static_cast<int>(stencilByte[0]), 200);
|
||||||
|
|
||||||
|
std::vector<int> stencilInt(pixels, kStencilPoison);
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_INT, stencilInt.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||||
|
EXPECT_EQ(stencilInt[0], 200);
|
||||||
|
|
||||||
|
DestroySource(source);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PACK pixel-store parameters apply to a depth read exactly as they do to a colour
|
||||||
|
// one, and the gap regions they create must be left alone.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, DepthReadbackHonoursThePackPixelStoreParameters) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH_COMPONENT24);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH_COMPONENT24, 0.5f, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const int rectWidth = 4;
|
||||||
|
const int rectHeight = 3;
|
||||||
|
const int rowLength = 8;
|
||||||
|
const int skipPixels = 2;
|
||||||
|
const int skipRows = 1;
|
||||||
|
constexpr float kGap = -7.0f;
|
||||||
|
std::vector<float> destination(static_cast<size_t>(rowLength) * (skipRows + rectHeight) + 16, kGap);
|
||||||
|
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, rowLength);
|
||||||
|
glPixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
|
||||||
|
glPixelStorei(GL_PACK_SKIP_ROWS, skipRows);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glReadPixels(0, 0, rectWidth, rectHeight, GL_DEPTH_COMPONENT, GL_FLOAT, destination.data());
|
||||||
|
const unsigned int readError = FirstGLError();
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||||
|
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
EXPECT_EQ(readError, 0u);
|
||||||
|
|
||||||
|
size_t written = 0;
|
||||||
|
size_t gapsTouched = 0;
|
||||||
|
for (size_t index = 0; index < destination.size(); ++index) {
|
||||||
|
const long row = static_cast<long>(index) / rowLength - skipRows;
|
||||||
|
const long column = static_cast<long>(index) % rowLength - skipPixels;
|
||||||
|
const bool inRect = row >= 0 && row < rectHeight && column >= 0 && column < rectWidth;
|
||||||
|
if (inRect) {
|
||||||
|
if (std::fabs(destination[index] - 0.5f) <= 1.0f / 4096.0f) ++written;
|
||||||
|
} else if (destination[index] != kGap) {
|
||||||
|
++gapsTouched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(written, static_cast<size_t>(rectWidth) * rectHeight)
|
||||||
|
<< "only " << written << " of " << (rectWidth * rectHeight)
|
||||||
|
<< " destination pixels landed where GL_PACK_ROW_LENGTH/SKIP_* put them";
|
||||||
|
EXPECT_EQ(gapsTouched, 0u) << gapsTouched << " bytes outside the packed rectangle were overwritten";
|
||||||
|
|
||||||
|
DestroySource(source);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The readback borrows the application's context for a full-screen pass. Everything it
|
||||||
|
// touches has to come back, or the next draw inherits it - which is how an emulation
|
||||||
|
// that "works" takes the rest of the renderer down with it.
|
||||||
|
TEST_F(DepthStencilReadbackMatrixScenario, ReadbackLeavesNoGLStateBehind) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||||
|
ASSERT_TRUE(SourceIsUsable());
|
||||||
|
FirstGLError();
|
||||||
|
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 4);
|
||||||
|
|
||||||
|
// A deliberately awkward state: nothing here is what an emulation pass would want,
|
||||||
|
// so anything it forgets to put back shows up below.
|
||||||
|
GLuint scratchTexture = 0;
|
||||||
|
glGenTextures(1, &scratchTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||||
|
glActiveTexture(GL_TEXTURE3);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(3, 5, 7, 11);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glDepthFunc(GL_GEQUAL);
|
||||||
|
glDepthMask(GL_FALSE);
|
||||||
|
glEnable(GL_STENCIL_TEST);
|
||||||
|
glStencilFunc(GL_NOTEQUAL, 0x5, 0x0Fu);
|
||||||
|
glStencilOp(GL_INCR, GL_DECR, GL_INVERT);
|
||||||
|
glStencilMask(0x3Cu);
|
||||||
|
glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
|
||||||
|
glViewport(2, 3, 5, 7);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||||
|
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
ExpectAllDepth(depth, 0.5f, "state-preservation case depth");
|
||||||
|
ExpectAllStencil(stencil, 4, "state-preservation case stencil");
|
||||||
|
|
||||||
|
GLint viewport[4] = {0, 0, 0, 0};
|
||||||
|
GLint scissorBox[4] = {0, 0, 0, 0};
|
||||||
|
GLboolean colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||||
|
GLint depthFunc = 0;
|
||||||
|
GLboolean depthMask = GL_TRUE;
|
||||||
|
GLint stencilFunc = 0, stencilRef = 0, stencilValueMask = 0, stencilWriteMask = 0;
|
||||||
|
GLint stencilFail = 0, stencilPassDepthFail = 0, stencilPassDepthPass = 0;
|
||||||
|
GLint activeTexture = 0, boundTexture = 0;
|
||||||
|
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||||
|
glGetIntegerv(GL_SCISSOR_BOX, scissorBox);
|
||||||
|
glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
|
||||||
|
glGetIntegerv(GL_DEPTH_FUNC, &depthFunc);
|
||||||
|
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
|
||||||
|
glGetIntegerv(GL_STENCIL_FUNC, &stencilFunc);
|
||||||
|
glGetIntegerv(GL_STENCIL_REF, &stencilRef);
|
||||||
|
glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask);
|
||||||
|
glGetIntegerv(GL_STENCIL_WRITEMASK, &stencilWriteMask);
|
||||||
|
glGetIntegerv(GL_STENCIL_FAIL, &stencilFail);
|
||||||
|
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &stencilPassDepthFail);
|
||||||
|
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &stencilPassDepthPass);
|
||||||
|
glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture);
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTexture);
|
||||||
|
|
||||||
|
EXPECT_EQ(viewport[0], 2);
|
||||||
|
EXPECT_EQ(viewport[1], 3);
|
||||||
|
EXPECT_EQ(viewport[2], 5);
|
||||||
|
EXPECT_EQ(viewport[3], 7);
|
||||||
|
EXPECT_EQ(scissorBox[0], 3);
|
||||||
|
EXPECT_EQ(scissorBox[1], 5);
|
||||||
|
EXPECT_EQ(scissorBox[2], 7);
|
||||||
|
EXPECT_EQ(scissorBox[3], 11);
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_SCISSOR_TEST), GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_CULL_FACE), GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_BLEND), GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_DEPTH_TEST), GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(glIsEnabled(GL_STENCIL_TEST), GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(colorMask[0], GLboolean(GL_FALSE));
|
||||||
|
EXPECT_EQ(colorMask[1], GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(colorMask[2], GLboolean(GL_FALSE));
|
||||||
|
EXPECT_EQ(colorMask[3], GLboolean(GL_TRUE));
|
||||||
|
EXPECT_EQ(depthFunc, GLint(GL_GEQUAL));
|
||||||
|
EXPECT_EQ(depthMask, GLboolean(GL_FALSE));
|
||||||
|
EXPECT_EQ(stencilFunc, GLint(GL_NOTEQUAL));
|
||||||
|
EXPECT_EQ(stencilRef, 0x5);
|
||||||
|
EXPECT_EQ(stencilValueMask, 0x0F);
|
||||||
|
EXPECT_EQ(stencilWriteMask, 0x3C);
|
||||||
|
EXPECT_EQ(stencilFail, GLint(GL_INCR));
|
||||||
|
EXPECT_EQ(stencilPassDepthFail, GLint(GL_DECR));
|
||||||
|
EXPECT_EQ(stencilPassDepthPass, GLint(GL_INVERT));
|
||||||
|
EXPECT_EQ(activeTexture, GLint(GL_TEXTURE3));
|
||||||
|
EXPECT_EQ(boundTexture, GLint(scratchTexture))
|
||||||
|
<< "the readback left a scratch texture on the application's texture unit";
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// Put the awkward state back so the next scenario in this process starts clean.
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_STENCIL_TEST);
|
||||||
|
glDepthFunc(GL_LESS);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFFu);
|
||||||
|
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||||
|
glStencilMask(0xFFFFFFFFu);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glDeleteTextures(1, &scratchTexture);
|
||||||
|
DestroySource(source);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -58,12 +58,12 @@ namespace MGITest {
|
|||||||
|
|
||||||
class DepthStencilReadbackScenario : public ScenarioTest {
|
class DepthStencilReadbackScenario : public ScenarioTest {
|
||||||
protected:
|
protected:
|
||||||
// DirectGLES reads depth and stencil back through the ES driver, which has no
|
// Both backends now answer these reads. DirectGLES has no native ES path for
|
||||||
// guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and
|
// either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on
|
||||||
// absent on both the Adreno device and Mesa's ES). That gap is tracked separately as
|
// both the Adreno device and Mesa's ES), so it stages the attachment into a
|
||||||
// the packed_depth_stencil cluster and needs a shader-sampling emulation, not this
|
// scratch depth texture and samples it into a colour target; the assertions below
|
||||||
// change; asserting it here would only pin a known-missing feature.
|
// are the same either way, which is the point.
|
||||||
bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; }
|
bool BackendReadsDepthStencil() const { return true; }
|
||||||
|
|
||||||
float ReadDepthAt(int x, int y) const {
|
float ReadDepthAt(int x, int y) const {
|
||||||
float depth = kDepthPoison;
|
float depth = kDepthPoison;
|
||||||
|
|||||||
@@ -6,27 +6,32 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
//
|
//
|
||||||
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
// Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
|
||||||
//
|
//
|
||||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||||
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
|
||||||
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
// type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||||
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
// profile") and the program never reaches the driver. On every such backend MobileGL narrows
|
||||||
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
|
||||||
// shader: `double` compiles and runs everywhere, at float precision.
|
// declining the shader: `double` compiles and runs everywhere, at float precision. Where the
|
||||||
|
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
|
||||||
|
// and the doubles reach the driver whole.
|
||||||
//
|
//
|
||||||
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
|
||||||
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
|
||||||
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
|
||||||
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
// std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
|
||||||
// silently - the uniform simply reads as something else - so the cases below set values
|
// silently - the uniform simply reads as something else - so the cases below set values through
|
||||||
// through the API and have the SHADER report what it saw.
|
// the API and have the SHADER report what it saw.
|
||||||
//
|
//
|
||||||
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
|
||||||
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
|
||||||
// which is the whole point.
|
// float32, so the same number is correct in both regimes and the assertions test the LAYOUT
|
||||||
|
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
|
||||||
|
// that is not - 0.1 - and it names both answers explicitly.
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -99,6 +104,8 @@ void main() {
|
|||||||
|
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
|
if (m_shapeOutput != 0) glDeleteBuffers(1, &m_shapeOutput);
|
||||||
|
if (m_shapeProgram != 0) glDeleteProgram(m_shapeProgram);
|
||||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
}
|
}
|
||||||
@@ -146,9 +153,292 @@ void main() {
|
|||||||
|
|
||||||
unsigned int m_program = 0;
|
unsigned int m_program = 0;
|
||||||
unsigned int m_output = 0;
|
unsigned int m_output = 0;
|
||||||
|
unsigned int m_shapeProgram = 0;
|
||||||
|
unsigned int m_shapeOutput = 0;
|
||||||
std::string m_buildLog;
|
std::string m_buildLog;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A SHADER STORAGE BLOCK that holds doubles is the one place the narrowing is NOT free:
|
||||||
|
// demoting `double` to `float` also repacks the block, and the bytes the application
|
||||||
|
// wrote into the buffer do not move with it. Every member past the first double then
|
||||||
|
// reads and writes at the wrong offset, and the block is simply shorter than the one
|
||||||
|
// that was bound - the tail of it is never touched at all
|
||||||
|
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3, whose output matched its
|
||||||
|
// input up to the first double's slot and was zero from there on).
|
||||||
|
//
|
||||||
|
// The block layout is fixed by GL 4.6 core 7.6.2.2 and is asserted here as literal byte
|
||||||
|
// offsets rather than queried, so this says what the SPEC requires and not what MobileGL
|
||||||
|
// happens to report. Both packings are covered because they differ in exactly the places
|
||||||
|
// that matter: std140 rounds an array's stride and a matrix's column stride up to 16,
|
||||||
|
// std430 does not, and only std430 packs the scalars tightly.
|
||||||
|
//
|
||||||
|
// Every value is exactly representable in binary32, so a correct implementation copies
|
||||||
|
// the block BYTE FOR BYTE even though it narrows each double on the way through.
|
||||||
|
constexpr const char* kBlockCopySource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std140, binding = 0) buffer In140 {
|
||||||
|
int data0;
|
||||||
|
float data1[3];
|
||||||
|
mat3x2 data2;
|
||||||
|
double data3;
|
||||||
|
double data4[2];
|
||||||
|
int data5;
|
||||||
|
dvec3 data6;
|
||||||
|
} g_in140;
|
||||||
|
layout(std430, binding = 1) buffer In430 {
|
||||||
|
int data0;
|
||||||
|
float data1[3];
|
||||||
|
mat3x2 data2;
|
||||||
|
double data3;
|
||||||
|
double data4[2];
|
||||||
|
int data5;
|
||||||
|
dvec3 data6;
|
||||||
|
} g_in430;
|
||||||
|
layout(std140, binding = 2) buffer Out140 {
|
||||||
|
int data0;
|
||||||
|
float data1[3];
|
||||||
|
mat3x2 data2;
|
||||||
|
double data3;
|
||||||
|
double data4[2];
|
||||||
|
int data5;
|
||||||
|
dvec3 data6;
|
||||||
|
} g_out140;
|
||||||
|
layout(std430, binding = 3) buffer Out430 {
|
||||||
|
int data0;
|
||||||
|
float data1[3];
|
||||||
|
mat3x2 data2;
|
||||||
|
double data3;
|
||||||
|
double data4[2];
|
||||||
|
int data5;
|
||||||
|
dvec3 data6;
|
||||||
|
} g_out430;
|
||||||
|
void main() {
|
||||||
|
g_out140.data0 = g_in140.data0;
|
||||||
|
for (int i = 0; i < 3; ++i) g_out140.data1[i] = g_in140.data1[i];
|
||||||
|
g_out140.data2 = g_in140.data2;
|
||||||
|
g_out140.data3 = g_in140.data3;
|
||||||
|
for (int i = 0; i < 2; ++i) g_out140.data4[i] = g_in140.data4[i];
|
||||||
|
g_out140.data5 = g_in140.data5;
|
||||||
|
g_out140.data6 = g_in140.data6;
|
||||||
|
|
||||||
|
g_out430.data0 = g_in430.data0;
|
||||||
|
for (int i = 0; i < 3; ++i) g_out430.data1[i] = g_in430.data1[i];
|
||||||
|
g_out430.data2 = g_in430.data2;
|
||||||
|
g_out430.data3 = g_in430.data3;
|
||||||
|
for (int i = 0; i < 2; ++i) g_out430.data4[i] = g_in430.data4[i];
|
||||||
|
g_out430.data5 = g_in430.data5;
|
||||||
|
g_out430.data6 = g_in430.data6;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// GL 4.6 core 7.6.2.2 rule by rule, for the block above.
|
||||||
|
// std140: an array's element stride and a matrix's column stride round up to 16, a
|
||||||
|
// double aligns to 8 and a dvec3 to 32.
|
||||||
|
// std430: the same without the rounding - so the scalars pack tightly and only the
|
||||||
|
// dvec3's 32-byte alignment leaves a hole.
|
||||||
|
struct BlockLayout {
|
||||||
|
int data0;
|
||||||
|
int data1;
|
||||||
|
int data1Stride;
|
||||||
|
int data2;
|
||||||
|
int data2ColumnStride;
|
||||||
|
int data3;
|
||||||
|
int data4;
|
||||||
|
int data4Stride;
|
||||||
|
int data5;
|
||||||
|
int data6;
|
||||||
|
int size;
|
||||||
|
};
|
||||||
|
constexpr BlockLayout kStd140{0, 16, 16, 64, 16, 112, 128, 16, 160, 192, 216};
|
||||||
|
constexpr BlockLayout kStd430{0, 4, 4, 16, 8, 40, 48, 8, 64, 96, 120};
|
||||||
|
|
||||||
|
void PokeInt(std::vector<unsigned char>& bytes, int offset, int value) {
|
||||||
|
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
||||||
|
}
|
||||||
|
void PokeFloat(std::vector<unsigned char>& bytes, int offset, float value) {
|
||||||
|
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
||||||
|
}
|
||||||
|
void PokeDouble(std::vector<unsigned char>& bytes, int offset, double value) {
|
||||||
|
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The block's contents, at the offsets the standard puts them. Padding stays zero, which
|
||||||
|
// is what makes a byte-for-byte comparison against the (zero-initialised) output buffer
|
||||||
|
// catch a member that landed somewhere it should not have.
|
||||||
|
std::vector<unsigned char> MakeBlockContents(const BlockLayout& layout) {
|
||||||
|
std::vector<unsigned char> bytes(static_cast<std::size_t>(layout.size), 0);
|
||||||
|
PokeInt(bytes, layout.data0, 1);
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
PokeFloat(bytes, layout.data1 + i * layout.data1Stride, 2.0f + static_cast<float>(i));
|
||||||
|
}
|
||||||
|
// Column-major, two rows per column.
|
||||||
|
for (int column = 0; column < 3; ++column) {
|
||||||
|
for (int row = 0; row < 2; ++row) {
|
||||||
|
PokeFloat(bytes, layout.data2 + column * layout.data2ColumnStride + row * 4,
|
||||||
|
5.0f + static_cast<float>(column * 2 + row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PokeDouble(bytes, layout.data3, 11.0);
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
PokeDouble(bytes, layout.data4 + i * layout.data4Stride, 12.0 + static_cast<double>(i));
|
||||||
|
}
|
||||||
|
PokeInt(bytes, layout.data5, 14);
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast<double>(i));
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names the first byte that differs, and which member owns it, so a failure is a
|
||||||
|
// diagnosis rather than "the buffer is wrong".
|
||||||
|
std::string DescribeOffset(const BlockLayout& layout, int offset) {
|
||||||
|
const std::pair<int, const char*> members[] = {
|
||||||
|
{layout.data0, "data0"}, {layout.data1, "data1"}, {layout.data2, "data2"},
|
||||||
|
{layout.data3, "data3"}, {layout.data4, "data4"}, {layout.data5, "data5"},
|
||||||
|
{layout.data6, "data6"}};
|
||||||
|
const char* owner = "(padding before data0)";
|
||||||
|
for (const auto& [start, name] : members) {
|
||||||
|
if (offset >= start) owner = name;
|
||||||
|
}
|
||||||
|
return std::string(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
|
||||||
|
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
|
||||||
|
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
|
||||||
|
// matrices, whose column stride and total size both change when the demotion turns a
|
||||||
|
// 64-bit column into a 32-bit one, and whose members therefore move every uniform
|
||||||
|
// declared after them.
|
||||||
|
//
|
||||||
|
// The shader reports every component separately rather than one pass/fail flag, because
|
||||||
|
// "the readback is wrong" is not a diagnosis: a wrong column stride, a wrong member
|
||||||
|
// offset and a wrong narrowing all fail the same single comparison, and only the
|
||||||
|
// component map says which.
|
||||||
|
// No #version here on purpose: it is handed over as a separate source string, the way
|
||||||
|
// the CTS case hands it over.
|
||||||
|
constexpr const char* kAllDoubleShapesSource = R"(
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
uniform double g_0;
|
||||||
|
uniform dvec2 g_1;
|
||||||
|
uniform dvec3 g_2;
|
||||||
|
uniform dvec4 g_3;
|
||||||
|
uniform dmat2 g_4;
|
||||||
|
uniform dmat2x3 g_5;
|
||||||
|
uniform dmat2x4 g_6;
|
||||||
|
uniform dmat3x2 g_7;
|
||||||
|
uniform dmat3 g_8;
|
||||||
|
uniform dmat3x4 g_9;
|
||||||
|
uniform dmat4x2 g_10;
|
||||||
|
uniform dmat4x3 g_11;
|
||||||
|
uniform dmat4 g_12;
|
||||||
|
layout(std430, binding = 0) buffer Output {
|
||||||
|
float g_out[];
|
||||||
|
};
|
||||||
|
void main() {
|
||||||
|
g_out[0] = float(g_0);
|
||||||
|
for (int i = 0; i < 2; ++i) g_out[1 + i] = float(g_1[i]);
|
||||||
|
for (int i = 0; i < 3; ++i) g_out[3 + i] = float(g_2[i]);
|
||||||
|
for (int i = 0; i < 4; ++i) g_out[6 + i] = float(g_3[i]);
|
||||||
|
for (int c = 0; c < 2; ++c) for (int r = 0; r < 2; ++r) g_out[10 + c * 2 + r] = float(g_4[c][r]);
|
||||||
|
for (int c = 0; c < 2; ++c) for (int r = 0; r < 3; ++r) g_out[14 + c * 3 + r] = float(g_5[c][r]);
|
||||||
|
for (int c = 0; c < 2; ++c) for (int r = 0; r < 4; ++r) g_out[20 + c * 4 + r] = float(g_6[c][r]);
|
||||||
|
for (int c = 0; c < 3; ++c) for (int r = 0; r < 2; ++r) g_out[28 + c * 2 + r] = float(g_7[c][r]);
|
||||||
|
for (int c = 0; c < 3; ++c) for (int r = 0; r < 3; ++r) g_out[34 + c * 3 + r] = float(g_8[c][r]);
|
||||||
|
for (int c = 0; c < 3; ++c) for (int r = 0; r < 4; ++r) g_out[43 + c * 4 + r] = float(g_9[c][r]);
|
||||||
|
for (int c = 0; c < 4; ++c) for (int r = 0; r < 2; ++r) g_out[55 + c * 2 + r] = float(g_10[c][r]);
|
||||||
|
for (int c = 0; c < 4; ++c) for (int r = 0; r < 3; ++r) g_out[63 + c * 3 + r] = float(g_11[c][r]);
|
||||||
|
for (int c = 0; c < 4; ++c) for (int r = 0; r < 4; ++r) g_out[75 + c * 4 + r] = float(g_12[c][r]);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The values the CTS case sets, spelled the way it spells them - column-major, and small
|
||||||
|
// enough that every one is exact in a float. Nothing here is a precision question; a
|
||||||
|
// component that comes back wrong came back from the wrong bytes.
|
||||||
|
constexpr double kG0 = 1.0;
|
||||||
|
constexpr double kG1[2] = {2.0, 3.0};
|
||||||
|
constexpr double kG2[3] = {4.0, 5.0, 6.0};
|
||||||
|
constexpr double kG3[4] = {7.0, 8.0, 9.0, 10.0};
|
||||||
|
constexpr double kG4[4] = {11.0, 12.0, 13.0, 14.0};
|
||||||
|
constexpr double kG5[6] = {15.0, 16.0, 17.0, 18.0, 19.0, 20.0};
|
||||||
|
constexpr double kG6[8] = {21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0};
|
||||||
|
constexpr double kG7[6] = {29.0, 30.0, 31.0, 32.0, 33.0, 34.0};
|
||||||
|
constexpr double kG8[9] = {35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0};
|
||||||
|
constexpr double kG9[12] = {44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0};
|
||||||
|
constexpr double kG10[8] = {56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0};
|
||||||
|
constexpr double kG11[12] = {63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73.0, 74.0};
|
||||||
|
constexpr double kG12[16] = {75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0,
|
||||||
|
83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0};
|
||||||
|
|
||||||
|
struct DoubleShape {
|
||||||
|
const char* name;
|
||||||
|
int base;
|
||||||
|
int columns; // 1 for the scalar and the vectors
|
||||||
|
int rows; // component count for the scalar and the vectors
|
||||||
|
const double* values;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr DoubleShape kDoubleShapes[] = {
|
||||||
|
{"g_0 double", 0, 1, 1, &kG0}, {"g_1 dvec2", 1, 1, 2, kG1},
|
||||||
|
{"g_2 dvec3", 3, 1, 3, kG2}, {"g_3 dvec4", 6, 1, 4, kG3},
|
||||||
|
{"g_4 dmat2", 10, 2, 2, kG4}, {"g_5 dmat2x3", 14, 2, 3, kG5},
|
||||||
|
{"g_6 dmat2x4", 20, 2, 4, kG6}, {"g_7 dmat3x2", 28, 3, 2, kG7},
|
||||||
|
{"g_8 dmat3", 34, 3, 3, kG8}, {"g_9 dmat3x4", 43, 3, 4, kG9},
|
||||||
|
{"g_10 dmat4x2", 55, 4, 2, kG10}, {"g_11 dmat4x3", 63, 4, 3, kG11},
|
||||||
|
{"g_12 dmat4", 75, 4, 4, kG12},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int kAllShapeSlots = 91;
|
||||||
|
|
||||||
|
// The conformance case's own shader, kept verbatim down to the literal suffixes and the
|
||||||
|
// unnamed, unqualified storage block - except that each comparison sets its OWN bit
|
||||||
|
// instead of collapsing all thirteen into one flag. That single flag is the whole reason
|
||||||
|
// the case was unexplained for a wave: it says "something is wrong" and nothing else.
|
||||||
|
//
|
||||||
|
// Verbatim matters here. Reading the components out one at a time (the case above)
|
||||||
|
// passes; whatever fails does so through the shape the conformance case actually
|
||||||
|
// writes - whole-matrix comparison against a constructor, a storage block with no
|
||||||
|
// layout qualifier and no instance name, values reached with constant indices.
|
||||||
|
constexpr const char* kCtsShapedSource = R"(
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
buffer Result {
|
||||||
|
int g_result;
|
||||||
|
};
|
||||||
|
uniform double g_0;
|
||||||
|
uniform dvec2 g_1;
|
||||||
|
uniform dvec3 g_2;
|
||||||
|
uniform dvec4 g_3;
|
||||||
|
uniform dmat2 g_4;
|
||||||
|
uniform dmat2x3 g_5;
|
||||||
|
uniform dmat2x4 g_6;
|
||||||
|
uniform dmat3x2 g_7;
|
||||||
|
uniform dmat3 g_8;
|
||||||
|
uniform dmat3x4 g_9;
|
||||||
|
uniform dmat4x2 g_10;
|
||||||
|
uniform dmat4x3 g_11;
|
||||||
|
uniform dmat4 g_12;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
g_result = 0;
|
||||||
|
|
||||||
|
if (g_0 != 1.0LF) g_result |= 1;
|
||||||
|
if (g_1 != dvec2(2.0LF, 3.0LF)) g_result |= 2;
|
||||||
|
if (g_2 != dvec3(4.0LF, 5.0LF, 6.0LF)) g_result |= 4;
|
||||||
|
if (g_3 != dvec4(7.0LF, 8.0LF, 9.0LF, 10.0LF)) g_result |= 8;
|
||||||
|
|
||||||
|
if (g_4 != dmat2(11.0LF, 12.0LF, 13.0LF, 14.0LF)) g_result |= 16;
|
||||||
|
if (g_5 != dmat2x3(15.0LF, 16.0LF, 17.0LF, 18.0LF, 19.0LF, 20.0LF)) g_result |= 32;
|
||||||
|
if (g_6 != dmat2x4(21.0LF, 22.0LF, 23.0LF, 24.0LF, 25.0LF, 26.0LF, 27.0LF, 28.0LF)) g_result |= 64;
|
||||||
|
|
||||||
|
if (g_7 != dmat3x2(29.0LF, 30.0LF, 31.0LF, 32.0LF, 33.0LF, 34.0LF)) g_result |= 128;
|
||||||
|
if (g_8 != dmat3(35.0LF, 36.0LF, 37.0LF, 38.0LF, 39.0LF, 40.0LF, 41.0LF, 42.0LF, 43.0LF)) g_result |= 256;
|
||||||
|
if (g_9 != dmat3x4(44.0LF, 45.0LF, 46.0LF, 47.0LF, 48.0LF, 49.0LF, 50.0LF, 51.0LF, 52.0LF, 53.0LF, 54.0LF, 55.0LF)) g_result |= 512;
|
||||||
|
|
||||||
|
if (g_10 != dmat4x2(56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0)) g_result |= 1024;
|
||||||
|
if (g_11 != dmat4x3(63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73, 74.0)) g_result |= 2048;
|
||||||
|
if (g_12 != dmat4(75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0)) g_result |= 4096;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
glUseProgram(m_program);
|
glUseProgram(m_program);
|
||||||
@@ -288,12 +578,24 @@ void main() {
|
|||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
|
|
||||||
// The readback has to undo exactly what the write did - the same std140 column
|
// The readback has to undo exactly what the write did - the same std140 column
|
||||||
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
// padding, the same component width - or a dmat4 comes back with its columns
|
||||||
// shifted and nothing else in the API would say so.
|
// shifted and nothing else in the API would say so. Every value below except the
|
||||||
|
// scalar is exact in float32, so those expectations pin the LAYOUT and hold in
|
||||||
|
// either regime; the scalar is the one that also pins the PRECISION.
|
||||||
GLdouble readScalar = 0.0;
|
GLdouble readScalar = 0.0;
|
||||||
glGetUniformdv(m_program, scalar, &readScalar);
|
glGetUniformdv(m_program, scalar, &readScalar);
|
||||||
|
// 0.1 is not representable in float32, so what comes back names the regime: a
|
||||||
|
// backend without native fp64 narrowed it at the glUniform1d above (the module's own
|
||||||
|
// doubles were demoted, so its storage is 4 bytes per component), and one with it
|
||||||
|
// stored the double whole. Both are correct; asserting only the narrow answer would
|
||||||
|
// fail the moment fp64 stops being emulated, and asserting only the wide one would
|
||||||
|
// fail on every mobile device there is.
|
||||||
|
if (readScalar == 0.1) {
|
||||||
|
SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
|
||||||
|
} else {
|
||||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||||
<< "the value is what a float can hold, not the double that was passed in";
|
<< "the value is what a float can hold, not the double that was passed in";
|
||||||
|
}
|
||||||
|
|
||||||
GLdouble readVector[3] = {};
|
GLdouble readVector[3] = {};
|
||||||
glGetUniformdv(m_program, vector, readVector);
|
glGetUniformdv(m_program, vector, readVector);
|
||||||
@@ -307,7 +609,8 @@ void main() {
|
|||||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The float query sees the same storage through the type it is actually stored as.
|
// The float query sees the same storage through a narrower type, and answers the
|
||||||
|
// same float either way: GL 4.6 core 7.6 converts on the way out.
|
||||||
GLfloat readFloat = 0.0f;
|
GLfloat readFloat = 0.0f;
|
||||||
glGetUniformfv(m_program, scalar, &readFloat);
|
glGetUniformfv(m_program, scalar, &readFloat);
|
||||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||||
@@ -353,6 +656,190 @@ void main() {
|
|||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DoublePrecisionScenario, EveryDoubleUniformShapeArrivesWhereTheShaderReadsIt) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// Built the way the CTS case builds it, because every step of that build has been a
|
||||||
|
// bug here at least once: the source arrives as TWO strings (the version directive
|
||||||
|
// and the body), the shader is attached before it has a source and deleted while
|
||||||
|
// still attached, and the program is linked twice.
|
||||||
|
m_shapeProgram = glCreateProgram();
|
||||||
|
ASSERT_NE(m_shapeProgram, 0u);
|
||||||
|
{
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glAttachShader(m_shapeProgram, shader);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
const char* const sources[2] = {"#version 430 core\n", kAllDoubleShapesSource};
|
||||||
|
glShaderSource(shader, 2, sources, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
FAIL() << "compute shader did not compile: " << log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glLinkProgram(m_shapeProgram);
|
||||||
|
{
|
||||||
|
GLint linkedOnce = 0;
|
||||||
|
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linkedOnce);
|
||||||
|
if (linkedOnce == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||||
|
FAIL() << "compute program did not link: " << log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenBuffers(1, &m_shapeOutput);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||||
|
const std::vector<float> zeroes(kAllShapeSlots, 0.0f);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, kAllShapeSlots * sizeof(float), zeroes.data(), GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||||
|
|
||||||
|
// Pass one sets through glProgramUniform*, pass two through glUniform* after a
|
||||||
|
// re-link - the two entry-point families the CTS case exercises, and two different
|
||||||
|
// routes into the same uniform storage.
|
||||||
|
const auto setWithProgramUniform = [&]() {
|
||||||
|
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||||
|
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||||
|
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||||
|
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||||
|
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||||
|
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||||
|
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||||
|
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||||
|
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||||
|
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||||
|
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||||
|
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||||
|
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||||
|
};
|
||||||
|
// Deliberately does NOT re-issue glUseProgram: the CTS case leaves the program
|
||||||
|
// current across the re-link and writes into it from there, so this is the path
|
||||||
|
// where a re-link has to keep the current program's uniform storage addressable.
|
||||||
|
const auto setWithUniform = [&]() {
|
||||||
|
glUniform1d(location("g_0"), kG0);
|
||||||
|
glUniform2d(location("g_1"), kG1[0], kG1[1]);
|
||||||
|
glUniform3d(location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||||
|
glUniform4d(location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||||
|
glUniformMatrix2dv(location("g_4"), 1, GL_FALSE, kG4);
|
||||||
|
glUniformMatrix2x3dv(location("g_5"), 1, GL_FALSE, kG5);
|
||||||
|
glUniformMatrix2x4dv(location("g_6"), 1, GL_FALSE, kG6);
|
||||||
|
glUniformMatrix3x2dv(location("g_7"), 1, GL_FALSE, kG7);
|
||||||
|
glUniformMatrix3dv(location("g_8"), 1, GL_FALSE, kG8);
|
||||||
|
glUniformMatrix3x4dv(location("g_9"), 1, GL_FALSE, kG9);
|
||||||
|
glUniformMatrix4x2dv(location("g_10"), 1, GL_FALSE, kG10);
|
||||||
|
glUniformMatrix4x3dv(location("g_11"), 1, GL_FALSE, kG11);
|
||||||
|
glUniformMatrix4dv(location("g_12"), 1, GL_FALSE, kG12);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto dispatchAndRead = [&]() {
|
||||||
|
glUseProgram(m_shapeProgram);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
std::vector<float> values(kAllShapeSlots, -1.0f);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), values.data());
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
// The program stays current on purpose - see setWithUniform.
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto expectEverything = [](const std::vector<float>& values, const char* pass) {
|
||||||
|
for (const DoubleShape& shape : kDoubleShapes) {
|
||||||
|
for (int c = 0; c < shape.columns; ++c) {
|
||||||
|
for (int r = 0; r < shape.rows; ++r) {
|
||||||
|
const int component = c * shape.rows + r;
|
||||||
|
EXPECT_FLOAT_EQ(values[shape.base + component],
|
||||||
|
static_cast<float>(shape.values[component]))
|
||||||
|
<< pass << ": " << shape.name << " column " << c << " row " << r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setWithProgramUniform();
|
||||||
|
expectEverything(dispatchAndRead(), "glProgramUniform*");
|
||||||
|
|
||||||
|
// A re-link zeroes every uniform, so pass two proves its own writes rather than
|
||||||
|
// reading pass one's bytes back.
|
||||||
|
glLinkProgram(m_shapeProgram);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||||
|
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), zeroes.data());
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
setWithUniform();
|
||||||
|
expectEverything(dispatchAndRead(), "glUniform* after re-link");
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DoublePrecisionScenario, TheConformanceUniformShaderAgreesWithEveryValueItWasGiven) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
m_shapeProgram = glCreateProgram();
|
||||||
|
ASSERT_NE(m_shapeProgram, 0u);
|
||||||
|
{
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glAttachShader(m_shapeProgram, shader);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
const char* const sources[2] = {"#version 430 core\n", kCtsShapedSource};
|
||||||
|
glShaderSource(shader, 2, sources, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
FAIL() << "compute shader did not compile: " << log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glLinkProgram(m_shapeProgram);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||||
|
FAIL() << "compute program did not link: " << log;
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenBuffers(1, &m_shapeOutput);
|
||||||
|
const GLint seed = 123;
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(seed), &seed, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||||
|
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||||
|
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||||
|
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||||
|
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||||
|
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||||
|
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||||
|
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||||
|
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||||
|
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||||
|
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||||
|
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||||
|
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||||
|
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||||
|
|
||||||
|
glUseProgram(m_shapeProgram);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
|
||||||
|
GLint disagreements = -1;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(disagreements), &disagreements);
|
||||||
|
for (int bit = 0; bit < 13; ++bit) {
|
||||||
|
EXPECT_EQ(disagreements & (1 << bit), 0)
|
||||||
|
<< kDoubleShapes[bit].name << " did not compare equal to the value it was given";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
// The shader above compiled, linked and ran without the extension string, which is
|
// The shader above compiled, linked and ran without the extension string, which is
|
||||||
@@ -373,24 +860,188 @@ void main() {
|
|||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
||||||
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
||||||
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
// ones whose device lacks shaderFloat64.
|
||||||
// drawn as garbage; the matching POST row says the same thing at startup.
|
//
|
||||||
|
// What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
|
||||||
|
// a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
|
||||||
|
// context MobileGL advertises, so refusing the call would be non-conformant and would
|
||||||
|
// leave four pure state queries unanswerable
|
||||||
|
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
|
||||||
|
// queryable; the enabled array is what gets dropped, and the attribute then reads its
|
||||||
|
// generic current value. The matching POST row says exactly that at startup.
|
||||||
GLuint vao = 0;
|
GLuint vao = 0;
|
||||||
glGenVertexArrays(1, &vao);
|
glGenVertexArrays(1, &vao);
|
||||||
glBindVertexArray(vao);
|
glBindVertexArray(vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
|
|
||||||
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "glVertexAttribLFormat is a legal call in a GL 4.3 context";
|
||||||
|
|
||||||
|
GLint attribSize = 0;
|
||||||
|
GLint attribType = 0;
|
||||||
|
GLint attribIsLong = 0;
|
||||||
|
GLint attribRelativeOffset = 0;
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
|
||||||
|
EXPECT_EQ(attribSize, 3);
|
||||||
|
EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
|
||||||
|
EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
|
||||||
|
"unconverted form; without it the state is a lie";
|
||||||
|
EXPECT_EQ(attribRelativeOffset, 8);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glDeleteVertexArrays(1, &vao);
|
glDeleteVertexArrays(1, &vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The consequence of recording the state rather than refusing the call: a 64-bit array can
|
||||||
|
// now be ENABLED in a VAO that a draw uses, which it never could before. That must not
|
||||||
|
// take the draw down. Leaving such an array enabled with no pointer behind it is exactly
|
||||||
|
// the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
|
||||||
|
// disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
|
||||||
|
// the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
|
||||||
|
//
|
||||||
|
// The shader deliberately does NOT read location 1: that keeps the two backends on the
|
||||||
|
// same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
|
||||||
|
// by design and loudly, which is a different assertion from this one) and it is the shape
|
||||||
|
// the crash needed - an enabled array nothing set a pointer for.
|
||||||
|
TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
constexpr const char* kVs = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
constexpr const char* kFs = R"(#version 430 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVs, kFs, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
ColorFbo target = MakeColorFbo(32, 32);
|
||||||
|
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
|
||||||
|
BindFbo(target);
|
||||||
|
|
||||||
|
const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
const double doubles[4] = {1.0, 2.0, 3.0, 4.0};
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
GLuint positionBuffer = 0;
|
||||||
|
GLuint doubleBuffer = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &positionBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
|
||||||
|
glGenBuffers(1, &doubleBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||||
|
glVertexAttribBinding(0, 0);
|
||||||
|
glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
|
||||||
|
glVertexAttribBinding(1, 1);
|
||||||
|
glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";
|
||||||
|
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";
|
||||||
|
|
||||||
|
const Image image = ReadPixels(target.width, target.height);
|
||||||
|
ASSERT_FALSE(image.Empty());
|
||||||
|
EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
|
||||||
|
<< "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";
|
||||||
|
|
||||||
|
glDisableVertexAttribArray(0);
|
||||||
|
glDisableVertexAttribArray(1);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &positionBuffer);
|
||||||
|
glDeleteBuffers(1, &doubleBuffer);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DoublePrecisionScenario, AStorageBlockWithDoublesKeepsTheLayoutItWasBoundWith) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
GLint blocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
|
||||||
|
if (blocks < 4) {
|
||||||
|
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 4";
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned int program = CompileComputeProgram(kBlockCopySource);
|
||||||
|
ASSERT_NE(program, 0u) << m_buildLog;
|
||||||
|
|
||||||
|
const std::vector<unsigned char> in140 = MakeBlockContents(kStd140);
|
||||||
|
const std::vector<unsigned char> in430 = MakeBlockContents(kStd430);
|
||||||
|
const std::vector<unsigned char> zero140(in140.size(), 0);
|
||||||
|
const std::vector<unsigned char> zero430(in430.size(), 0);
|
||||||
|
|
||||||
|
GLuint buffers[4] = {};
|
||||||
|
glGenBuffers(4, buffers);
|
||||||
|
const std::vector<unsigned char>* contents[4] = {&in140, &in430, &zero140, &zero430};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), buffers[i]);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(contents[i]->size()),
|
||||||
|
contents[i]->data(), GL_DYNAMIC_COPY);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
|
const BlockLayout& layout = pass == 0 ? kStd140 : kStd430;
|
||||||
|
const std::vector<unsigned char>& expected = pass == 0 ? in140 : in430;
|
||||||
|
const char* packing = pass == 0 ? "std140" : "std430";
|
||||||
|
std::vector<unsigned char> observed(expected.size(), 0xEE);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||||
|
static_cast<GLsizeiptr>(observed.size()), observed.data());
|
||||||
|
int mismatches = 0;
|
||||||
|
int firstMismatch = -1;
|
||||||
|
for (std::size_t i = 0; i < expected.size(); ++i) {
|
||||||
|
if (expected[i] == observed[i]) continue;
|
||||||
|
++mismatches;
|
||||||
|
if (firstMismatch < 0) firstMismatch = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(mismatches, 0)
|
||||||
|
<< packing << " block: " << mismatches << " of " << expected.size()
|
||||||
|
<< " bytes differ, first at byte " << firstMismatch << " (in "
|
||||||
|
<< DescribeOffset(layout, firstMismatch < 0 ? 0 : firstMismatch)
|
||||||
|
<< "); a block that was repacked around its doubles reads and writes every "
|
||||||
|
"member after the first one at the wrong offset";
|
||||||
|
}
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteBuffers(4, buffers);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FormatlessImageBakeScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A FORMAT-LESS IMAGE UNIFORM WHOSE UNIT HOLDS A NON-CORE FORMAT.
|
||||||
|
//
|
||||||
|
// GLSL 4.20 lets a write-only image uniform omit its layout format; GLSL ES demands one, so
|
||||||
|
// DirectGLES BAKES the format of whatever glBindImageTexture put on the unit into the
|
||||||
|
// declaration. When that format is outside the GLSL ES core thirteen, the bake alone is not
|
||||||
|
// enough - the baked declaration then has to go through the same channel-widening
|
||||||
|
// WidenImageFormatsForEssl gives a DECLARED non-core format (see NonCoreImageFormatScenario for
|
||||||
|
// the widening itself).
|
||||||
|
//
|
||||||
|
// The two routes had different arming. The declared route armed the widening on the format
|
||||||
|
// alone; the baked route armed it only when the driver lacked GL_NV_image_formats. That reads
|
||||||
|
// like an optimisation and is not one: SPIRV-Cross throws for its is_desktop_only_format set the
|
||||||
|
// moment it targets ESSL, whatever the driver would have accepted, so on a driver that HAS the
|
||||||
|
// extension the shader half of the widening stayed switched off while TextureImpl's storage/bind
|
||||||
|
// half - which keys on SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran. The
|
||||||
|
// stage threw, the program linked without it, and every dispatch silently did nothing.
|
||||||
|
//
|
||||||
|
// KHR-GL43.stencil_texturing.functional is where it surfaced: its compute half writes through a
|
||||||
|
// format-less `uimage2D` bound to an R8UI texture, and returned zeros for every texel.
|
||||||
|
//
|
||||||
|
// DISCRIMINATING ONLY WHERE THE DRIVER ADVERTISES GL_NV_image_formats - Mesa does, which is what
|
||||||
|
// the software lanes run and where this was found. On Adreno 830 and both Malis the extension is
|
||||||
|
// absent, the old code already armed the widening, and these cases pass before and after; they
|
||||||
|
// are kept running there as a guard against the opposite mistake.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kExtent = 8;
|
||||||
|
|
||||||
|
// No layout format on uni_image on purpose: that is the whole subject. uni_source is a
|
||||||
|
// plain integer texture so nothing but the image declaration is in play.
|
||||||
|
const char* const kComputeSource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
writeonly uniform uimage2D uni_image;
|
||||||
|
uniform usampler2D uni_source;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
imageStore(uni_image, at, uvec4(texelFetch(uni_source, at, 0).r, 0u, 0u, 0u));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class FormatlessImageBakeScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsCompute()) {
|
||||||
|
GTEST_SKIP() << "no compute stage on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool BackendHostsCompute() {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
DrainErrors();
|
||||||
|
return maxImageUnits >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint BuildCompute(const char* source, std::string& log) {
|
||||||
|
const GLuint cs = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(cs, 1, &source, nullptr);
|
||||||
|
glCompileShader(cs);
|
||||||
|
GLint ok = 0;
|
||||||
|
glGetShaderiv(cs, GL_COMPILE_STATUS, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
char buffer[2048] = "";
|
||||||
|
glGetShaderInfoLog(cs, sizeof(buffer), nullptr, buffer);
|
||||||
|
log = buffer;
|
||||||
|
glDeleteShader(cs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, cs);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
||||||
|
glDeleteShader(cs);
|
||||||
|
if (!ok) {
|
||||||
|
char buffer[2048] = "";
|
||||||
|
glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer);
|
||||||
|
log = buffer;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// internalFormat is the NON-CORE image format under test; the destination texture and
|
||||||
|
// the glBindImageTexture argument both use it, and the shader declares nothing.
|
||||||
|
void RunCopy(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType) {
|
||||||
|
std::vector<GLuint> expected(kExtent * kExtent);
|
||||||
|
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||||
|
expected[i] = static_cast<GLuint>(1 + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source: a core-format integer texture holding 1..64.
|
||||||
|
std::vector<GLubyte> sourceBytes(kExtent * kExtent);
|
||||||
|
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||||
|
sourceBytes[i] = static_cast<GLubyte>(expected[i]);
|
||||||
|
}
|
||||||
|
GLuint sourceTexture = 0;
|
||||||
|
glGenTextures(1, &sourceTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, kExtent, kExtent);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_BYTE,
|
||||||
|
sourceBytes.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
// Destination: the format under test, zero-filled so "the dispatch did nothing"
|
||||||
|
// and "the dispatch wrote zeros" are the same observation the CTS made.
|
||||||
|
GLuint destTexture = 0;
|
||||||
|
glGenTextures(1, &destTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||||
|
const std::vector<GLubyte> zeros(static_cast<std::size_t>(kExtent) * kExtent * 8, 0);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, zeros.data());
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "destination storage";
|
||||||
|
|
||||||
|
std::string log;
|
||||||
|
const GLuint program = BuildCompute(kComputeSource, log);
|
||||||
|
ASSERT_NE(program, 0u) << "the format-less image program did not build: " << log;
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
glBindImageTexture(1, destTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, internalFormat);
|
||||||
|
glUniform1i(glGetUniformLocation(program, "uni_image"), 1);
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||||
|
glUniform1i(glGetUniformLocation(program, "uni_source"), 1);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "binding";
|
||||||
|
|
||||||
|
glDispatchCompute(kExtent, kExtent, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "dispatch";
|
||||||
|
|
||||||
|
std::vector<GLuint> readback(kExtent * kExtent, 0xFFFFFFFFu);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, destTexture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, readback.data());
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "readback";
|
||||||
|
|
||||||
|
int offenders = 0;
|
||||||
|
for (int i = 0; i < kExtent * kExtent; ++i) {
|
||||||
|
if (readback[i] != expected[i]) ++offenders;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(offenders, 0) << "the dispatch wrote " << offenders << " of "
|
||||||
|
<< (kExtent * kExtent) << " texels wrongly; texel 0 was "
|
||||||
|
<< readback[0] << ", expected " << expected[0]
|
||||||
|
<< ". A whole stage lost to the ESSL emitter looks exactly like this.";
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteTextures(1, &sourceTexture);
|
||||||
|
glDeleteTextures(1, &destTexture);
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// R8UI: one of the seven formats GLSL ES reaches only through GL_NV_image_formats AND one
|
||||||
|
// SPIRV-Cross refuses to print for ESSL, so it needs the widening in both driver modes.
|
||||||
|
TEST_F(FormatlessImageBakeScenario, R8uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
RunCopy(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// R16UI, from the same set, carried in RGBA16UI: the fix must not be R8UI-shaped.
|
||||||
|
TEST_F(FormatlessImageBakeScenario, R16uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
RunCopy(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control: R32UI is in the GLSL ES core thirteen, so it is baked and never widened.
|
||||||
|
// It passed before the fix and has to keep passing.
|
||||||
|
TEST_F(FormatlessImageBakeScenario, CoreFormatBakedFromTheBoundUnitIsUnaffected) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
RunCopy(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
|
||||||
|
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
|
||||||
|
//
|
||||||
|
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
|
||||||
|
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
|
||||||
|
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
|
||||||
|
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
|
||||||
|
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
|
||||||
|
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
|
||||||
|
//
|
||||||
|
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
|
||||||
|
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
|
||||||
|
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
|
||||||
|
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
|
||||||
|
// program is the control that proves the rule was not simply widened.
|
||||||
|
//
|
||||||
|
// Needs a real context: the validator returns before this rule when no backend object is
|
||||||
|
// active, so the GPU-free negative-API suite cannot reach it.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const char* const kVertexSource = R"(#version 420 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
|
||||||
|
// `result` is here so the same program can be captured with transform feedback.
|
||||||
|
const char* const kPointsInGeometrySource = R"(#version 420 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
out float result;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
result = 1.0;
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kLinesInGeometrySource = R"(#version 420 core
|
||||||
|
layout(lines) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kFragmentSource = R"(#version 420 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class GeometryDrawModeScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
if (!BackendHostsGeometry()) {
|
||||||
|
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << "); there is no input primitive to validate";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
|
||||||
|
// driver without GL_EXT_geometry_shader and on a DirectVulkan device without the
|
||||||
|
// geometryShader feature.
|
||||||
|
static bool BackendHostsGeometry() {
|
||||||
|
GLint maxGeometryOutputVertices = 0;
|
||||||
|
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||||
|
DrainErrors();
|
||||||
|
return maxGeometryOutputVertices >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
|
||||||
|
const std::vector<std::pair<GLenum, const char*>> stages = {
|
||||||
|
{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_GEOMETRY_SHADER, geometrySource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||||
|
|
||||||
|
std::vector<GLuint> shaders;
|
||||||
|
bool ok = true;
|
||||||
|
for (const auto& [stage, source] : stages) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
shaders.push_back(shader);
|
||||||
|
if (!compiled) {
|
||||||
|
m_buildLog = InfoLog(shader, true);
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||||
|
if (capturedVarying != nullptr) {
|
||||||
|
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string InfoLog(GLuint object, bool isShader) {
|
||||||
|
GLint length = 0;
|
||||||
|
if (isShader) {
|
||||||
|
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
} else {
|
||||||
|
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
}
|
||||||
|
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||||
|
if (isShader) {
|
||||||
|
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||||
|
} else {
|
||||||
|
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||||
|
}
|
||||||
|
return buffer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& BuildLog() const { return m_buildLog; }
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
// GL_POINTS is the only mode that decomposes into a points input primitive.
|
||||||
|
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
const GLuint program = BuildProgram(kPointsInGeometrySource);
|
||||||
|
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
for (const GLenum mode :
|
||||||
|
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||||
|
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
|
||||||
|
glDrawArrays(mode, 0, 3);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||||
|
<< "mode " << mode << " does not decompose into the geometry shader's points input";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one mode that IS compatible still draws.
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
|
||||||
|
// case asks about. The capture span is really completed first, so GL_POINTS comes back
|
||||||
|
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
|
||||||
|
// never-ended-a-span reason instead and the case could not tell the two apart.
|
||||||
|
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
|
||||||
|
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
GLuint feedback = 0;
|
||||||
|
glGenTransformFeedbacks(1, &feedback);
|
||||||
|
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
|
||||||
|
GLuint captureBuffer = 0;
|
||||||
|
glGenBuffers(1, &captureBuffer);
|
||||||
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
glBeginTransformFeedback(GL_POINTS);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glEndTransformFeedback();
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
|
||||||
|
|
||||||
|
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||||
|
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||||
|
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
// The compatible mode replays the captured span with no error at all, which is what
|
||||||
|
// makes the two assertions above about the MODE and not about the span.
|
||||||
|
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "a compatible mode must still replay the captured span";
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||||
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &captureBuffer);
|
||||||
|
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
|
||||||
|
glDeleteTransformFeedbacks(1, &feedback);
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
|
||||||
|
// the rule even before the fix. It must still accept the line modes and still reject the
|
||||||
|
// others - a fix that widened the rule instead of repairing its guard breaks this.
|
||||||
|
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
const GLuint program = BuildProgram(kLinesInGeometrySource);
|
||||||
|
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
||||||
|
static_cast<GLenum>(GL_LINE_LOOP)}) {
|
||||||
|
glDrawArrays(mode, 0, 2);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "mode " << mode << " decomposes into lines and must be accepted";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
|
||||||
|
glDrawArrays(mode, 0, 3);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||||
|
<< "mode " << mode << " does not decompose into lines";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half of "ask the stage": WHICH stage list is asked. gsInputPrimitive is a
|
||||||
|
// LINK artifact, so pairing it with the live attach list re-points the GL_NONE/GL_POINTS
|
||||||
|
// aliasing instead of removing it - inside the window between glAttachShader and the
|
||||||
|
// next link, the live list says "geometry present" while the artifact still reads
|
||||||
|
// GL_NONE, which is 0, which is GL_POINTS, so every mode but GL_POINTS is rejected.
|
||||||
|
//
|
||||||
|
// GL 4.6 core 7.3 makes that window legal and ordinary: an attach affects the program's
|
||||||
|
// executable only at the next link, and leaves LINK_STATUS alone. The attached shader
|
||||||
|
// need not even compile. Worse, it does not heal - glDetachShader defers the removal to
|
||||||
|
// the next Link() too, so the program would keep failing every non-POINTS draw until the
|
||||||
|
// application happened to relink for some unrelated reason.
|
||||||
|
TEST_F(GeometryDrawModeScenario, AttachingAGeometryStageAfterTheLinkDoesNotConstrainTheDrawMode) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
// Deliberately NOT BuildProgram: the executable under test has no geometry stage.
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
m_programs.push_back(program);
|
||||||
|
for (const auto& [stage, source] :
|
||||||
|
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "a program with no geometry stage must draw triangles";
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
const GLuint geometry = glCreateShader(GL_GEOMETRY_SHADER);
|
||||||
|
glShaderSource(geometry, 1, &kPointsInGeometrySource, nullptr);
|
||||||
|
glCompileShader(geometry);
|
||||||
|
glAttachShader(program, geometry);
|
||||||
|
glDeleteShader(geometry);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
// Same executable as three lines ago - no relink has happened.
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "the attach does not reach the executable until the next link, so the geometry "
|
||||||
|
"shader's points input must not constrain this draw";
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
// And once it IS linked in, the rule applies - the fix must not have simply disabled it.
|
||||||
|
glLinkProgram(program);
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE) << "the relink with the geometry stage failed";
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
||||||
|
<< "now that the points-in geometry shader is in the executable, triangles must be rejected";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tessellation guard above the geometry one had the identical defect, and it does not
|
||||||
|
// even need the GL_NONE aliasing to misfire: it drives BOTH directions unconditionally, so
|
||||||
|
// reading the live attach list rejects every non-GL_PATCHES draw the moment an evaluation
|
||||||
|
// shader is attached, whether or not it was ever linked in.
|
||||||
|
TEST_F(GeometryDrawModeScenario, AttachingATessEvalStageAfterTheLinkDoesNotForceGlPatches) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
GLint maxPatchVertices = 0;
|
||||||
|
glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||||
|
DrainErrors();
|
||||||
|
if (maxPatchVertices < 3) GTEST_SKIP() << "no tessellation stage on this backend";
|
||||||
|
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
m_programs.push_back(program);
|
||||||
|
for (const auto& [stage, source] :
|
||||||
|
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
||||||
|
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
static const char* const kTessEvalSource = R"(#version 420 core
|
||||||
|
layout(triangles, equal_spacing, ccw) in;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint tessEval = glCreateShader(GL_TESS_EVALUATION_SHADER);
|
||||||
|
glShaderSource(tessEval, 1, &kTessEvalSource, nullptr);
|
||||||
|
glCompileShader(tessEval);
|
||||||
|
glAttachShader(program, tessEval);
|
||||||
|
glDeleteShader(tessEval);
|
||||||
|
DrainErrors();
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "the executable still has no tessellation stage, so GL_PATCHES must not be required";
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -179,6 +179,13 @@ void main()
|
|||||||
in flat uint v_index;
|
in flat uint v_index;
|
||||||
out vec4 o_color;
|
out vec4 o_color;
|
||||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The colour index spelled out at its default value. Says nothing that
|
||||||
|
// `layout(location = 0)` alone does not, and must therefore cost nothing.
|
||||||
|
constexpr const char* kExplicitColorIndexFS = R"(#version 420 core
|
||||||
|
layout(location = 0, index = 0) out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
)";
|
)";
|
||||||
|
|
||||||
class Glsl420DeclarationScenario : public ScenarioTest {
|
class Glsl420DeclarationScenario : public ScenarioTest {
|
||||||
@@ -473,4 +480,24 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `layout(location = 0, index = 0)` is the GL default written out loud, and an application
|
||||||
|
// is entitled to write it - KHR-GL43.shader_atomic_counters.basic-program-query does. It has
|
||||||
|
// to reach the driver as an ORDINARY single-source output: GLSL ES has no `index` qualifier
|
||||||
|
// in core, so a transpiler that prints the decoration back gets "index layout qualifier
|
||||||
|
// requires EXT_blend_func_extended", the stage never compiles, the program runs with a stage
|
||||||
|
// missing and the draw paints nothing at all. Black, not red - which is why the conformance
|
||||||
|
// case looked like the atomic counters had stopped counting.
|
||||||
|
TEST_F(Glsl420DeclarationScenario, AnExplicitDefaultColorIndexStillDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint program = Build(kQuadVS, kExplicitColorIndexFS);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndRead(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(centre.g, 255) << "a fragment output declared layout(location = 0, index = 0) painted "
|
||||||
|
"nothing; its stage was almost certainly refused by the driver";
|
||||||
|
EXPECT_EQ(centre.r, 0u);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.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 IMAGE UNIFORM THAT DECLARES NO FORMAT.
|
||||||
|
//
|
||||||
|
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
|
||||||
|
//
|
||||||
|
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
|
||||||
|
//
|
||||||
|
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
|
||||||
|
// images have to define layout format", which fails the whole program. That is what took the
|
||||||
|
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
|
||||||
|
//
|
||||||
|
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||||
|
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
|
||||||
|
// internal format to belong to one format class - so the format is not knowable when the shader
|
||||||
|
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
|
||||||
|
// generates and keys that program on the (unit, format) pairs it baked
|
||||||
|
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
|
||||||
|
//
|
||||||
|
// Three separate things follow from "the program is built against live binding state", and each
|
||||||
|
// one is a case below:
|
||||||
|
//
|
||||||
|
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
|
||||||
|
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
|
||||||
|
// compiled against the old format (RebindToADifferentFormatRebuilds);
|
||||||
|
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
|
||||||
|
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
|
||||||
|
//
|
||||||
|
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
|
||||||
|
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
|
||||||
|
// at descriptor time - so every case here runs on both backends and must agree, which is what
|
||||||
|
// makes the ES-only machinery falsifiable rather than merely exercised.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kExtent = 4;
|
||||||
|
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
|
||||||
|
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
|
||||||
|
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
|
||||||
|
constexpr GLint kImageUnit = 1;
|
||||||
|
|
||||||
|
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
|
||||||
|
const char* kStoreSource = R"(#version 430 core
|
||||||
|
|
||||||
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
|
||||||
|
writeonly uniform uimage2D uni_image;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ImageFormatQualifierScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||||
|
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||||
|
m_programs.clear();
|
||||||
|
m_textures.clear();
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||||
|
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
}
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeComputeProgram(const std::string& source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
const char* text = source.c_str();
|
||||||
|
glShaderSource(shader, 1, &text, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
m_programs.push_back(program);
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeTexture(GLenum internalFormat) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Seeded to a value no dispatch writes, so "the store never happened" and "the
|
||||||
|
// store wrote the right thing" cannot be confused.
|
||||||
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
|
||||||
|
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||||
|
zeros.data());
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
|
||||||
|
// than through a second image uniform: a defect in the format bake would be shared by
|
||||||
|
// a reader declared the same way and could cancel itself out.
|
||||||
|
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
|
||||||
|
const bool rgba = internalFormat == GL_RGBA32UI;
|
||||||
|
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
|
||||||
|
0xFFFFFFFFu);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||||
|
texels.data());
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
|
||||||
|
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
|
||||||
|
internalFormat);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
||||||
|
glUseProgram(program);
|
||||||
|
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||||
|
ASSERT_GE(location, 0) << "the image uniform was not reflected";
|
||||||
|
glUniform1i(location, kImageUnit);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
|
||||||
|
glDispatchCompute(kExtent, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The defect itself. Without the bake the ES driver refuses the program outright and the
|
||||||
|
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
|
||||||
|
// like, and why the seed is a value no store writes.
|
||||||
|
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
|
||||||
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||||
|
const GLuint texture = MakeTexture(GL_R32UI);
|
||||||
|
if (program == 0 || texture == 0) return;
|
||||||
|
|
||||||
|
DispatchStore(program, texture, GL_R32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "texel " << x << " of a format-less writeonly image did not take the store";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rebuild key. The SAME program is dispatched twice with a different format bound to
|
||||||
|
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
|
||||||
|
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
|
||||||
|
//
|
||||||
|
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
|
||||||
|
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
|
||||||
|
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
|
||||||
|
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
|
||||||
|
// program was built with no format at all and never compiled. Both are kept: this one is
|
||||||
|
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
|
||||||
|
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
|
||||||
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||||
|
const GLuint first = MakeTexture(GL_R32UI);
|
||||||
|
const GLuint second = MakeTexture(GL_RGBA32UI);
|
||||||
|
if (program == 0 || first == 0 || second == 0) return;
|
||||||
|
|
||||||
|
DispatchStore(program, first, GL_R32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "the first format must work before the rebind can be blamed for anything";
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchStore(program, second, GL_RGBA32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and back, so the rebuild is not a one-way door: returning to a format the
|
||||||
|
// program was once built against must build for it again, not resurrect a cache row.
|
||||||
|
const GLuint third = MakeTexture(GL_R32UI);
|
||||||
|
if (third == 0) return;
|
||||||
|
DispatchStore(program, third, GL_R32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "texel " << x << ": going back to the first format did not rebuild";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing is bound to the unit when the program links, so whatever the first build sees
|
||||||
|
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
|
||||||
|
// build - it is an entry point, and building there is the constraint
|
||||||
|
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
|
||||||
|
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
|
||||||
|
// build attempted from the entry point before the state settles.
|
||||||
|
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
|
||||||
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
// Use it once with NOTHING bound to the unit, which is what makes the backend build
|
||||||
|
// against an empty binding. The dispatch writes nowhere and must not error.
|
||||||
|
glUseProgram(program);
|
||||||
|
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||||
|
ASSERT_GE(location, 0);
|
||||||
|
glUniform1i(location, kImageUnit);
|
||||||
|
glDispatchCompute(kExtent, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
const GLuint texture = MakeTexture(GL_R32UI);
|
||||||
|
if (texture == 0) return;
|
||||||
|
DispatchStore(program, texture, GL_R32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "texel " << x << ": the first bind after the link did not reach the shader";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A DECLARED format is authoritative and the bake must never touch it - including when
|
||||||
|
// the texture behind the unit has a different (but class-compatible) internal format,
|
||||||
|
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
|
||||||
|
// that would go wrong while every other one stayed green.
|
||||||
|
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
|
||||||
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
||||||
|
|
||||||
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
|
||||||
|
layout (r32ui) writeonly uniform uimage2D uni_image;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
const GLuint texture = MakeTexture(GL_R32UI);
|
||||||
|
if (program == 0 || texture == 0) return;
|
||||||
|
|
||||||
|
DispatchStore(program, texture, GL_R32UI);
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||||
|
<< "texel " << x << ": a declared format stopped working";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -127,14 +127,23 @@ void main()
|
|||||||
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||||
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||||
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||||
// spelling in a single declaration and cannot be expressed at all without splitting
|
// spelling in a single declaration.
|
||||||
// the array into one declaration per element and rewriting every use of it.
|
|
||||||
//
|
//
|
||||||
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
// RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
|
||||||
// storage-block rebinding cases: the defect is per-backend and the frontend
|
// image uniform per element, each carrying its own binding, which costs exactly the
|
||||||
// mechanism these cases exist for - per-element units surviving the trip to the
|
// four image uniforms the application declared. (It used to WIDEN the array to cover
|
||||||
// pipeline composite - is fully exercised on Magma.
|
// the whole span instead, which cost seven for those four elements and had to be
|
||||||
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
// declined on a stage that could not afford them - hence the budget gate that used
|
||||||
|
// to be here.) DirectVulkan needs no rewrite at all.
|
||||||
|
bool PerElementImageUnitsAreHonoured() const {
|
||||||
|
if (Gl().BackendName() == "DirectVulkan") return true;
|
||||||
|
GLint maxFragmentImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
// One per element of the four-element array either fragment program declares.
|
||||||
|
return maxFragmentImageUniforms >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
// The scenarios below need image load/store at all; a driver without it should skip
|
// The scenarios below need image load/store at all; a driver without it should skip
|
||||||
// rather than fail.
|
// rather than fail.
|
||||||
@@ -164,7 +173,7 @@ void main()
|
|||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
if (!PerElementImageUnitsAreHonoured()) {
|
if (!PerElementImageUnitsAreHonoured()) {
|
||||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
GTEST_SKIP() << "fewer than 4 fragment image uniforms: the array under test does not fit";
|
||||||
}
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
@@ -283,8 +292,12 @@ void main()
|
|||||||
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
if (!PerElementImageUnitsAreHonoured()) {
|
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
|
||||||
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
// here are consecutive on purpose, so on Espryt this would exercise nothing the case
|
||||||
|
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
|
||||||
|
// it deliberately never triggers.
|
||||||
|
if (Gl().BackendName() != "DirectVulkan") {
|
||||||
|
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
|
||||||
}
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A DRAW READS imageSize() AFTER THE IMAGE TEXTURE IS RE-SPECIFIED.
|
||||||
|
//
|
||||||
|
// KHR-GL43.shader_image_size.advanced-changeSize reduced to its mechanism. The application binds
|
||||||
|
// a texture to an image unit ONCE, draws, then re-specifies that same texture with a new size
|
||||||
|
// through glTexImage2D and draws again - without touching the image unit. GL says the unit
|
||||||
|
// references the texture OBJECT, so the second draw must see the new dimensions.
|
||||||
|
//
|
||||||
|
// On Espryt it did not, and the reason is two facts meeting:
|
||||||
|
//
|
||||||
|
// 1. ES 3.1 only allows IMMUTABLE storage on an image unit, so the backend forces glTexStorage
|
||||||
|
// backing on any texture that reaches one (SyncTextureObjectToBackend's
|
||||||
|
// imageBindableStorageRequired). Immutable storage cannot be redefined, so a glTexImage2D
|
||||||
|
// that changes size or format has to MINT A NEW ES TEXTURE NAME.
|
||||||
|
// 2. The draw path never re-issued glBindImageTexture. Image units were established eagerly,
|
||||||
|
// once, when the application called glBindImageTexture, and PrepareForDraw only ever
|
||||||
|
// re-synced SAMPLED textures - so the unit kept pointing at the deleted name and
|
||||||
|
// imageSize() reported whatever that stale binding still meant.
|
||||||
|
//
|
||||||
|
// A dispatch was never affected: PrepareForCompute has always swept the image units. This is a
|
||||||
|
// draw-path scenario for exactly that reason - a compute-shaped case cannot see the defect.
|
||||||
|
//
|
||||||
|
// Both backends run it. Magma re-derives its image descriptors per draw and so was never wrong
|
||||||
|
// here, which makes it the control: the two backends have to agree on what the second draw sees.
|
||||||
|
|
||||||
|
#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 kTargetSize = 8;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 430 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// A single triangle that covers the whole target, with no vertex buffer at all: the
|
||||||
|
// scenario is about the image unit, so nothing else may be able to make it fail.
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 3.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 3.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Green when the image the unit currently holds has the size the application last gave
|
||||||
|
// it, red otherwise - the conformance case's own comparison, and its own colours.
|
||||||
|
constexpr const char* kFS = R"(#version 430 core
|
||||||
|
layout(rgba8) readonly uniform image2D g_image;
|
||||||
|
uniform ivec2 g_expected_size;
|
||||||
|
layout(location = 0) out vec4 o_color;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
o_color = (imageSize(g_image) == g_expected_size) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ImageSizeAfterRespecScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||||
|
if (m_color != 0) glDeleteTextures(1, &m_color);
|
||||||
|
if (m_image != 0) glDeleteTextures(1, &m_image);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_program = m_fbo = m_color = m_image = m_vao = 0;
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageSize() needs a fragment-stage image uniform; a driver that serves none should
|
||||||
|
// skip rather than fail.
|
||||||
|
bool FragmentImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
GLint maxFragmentImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 1 && maxFragmentImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeProgram() {
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVS, nullptr);
|
||||||
|
glShaderSource(fs, 1, &kFS, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
glCompileShader(fs);
|
||||||
|
for (const GLuint shader : {vs, fs}) {
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "a shader did not compile: " << log;
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the program did not link: " << log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MakeRenderTarget() {
|
||||||
|
glGenTextures(1, &m_color);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_color);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTargetSize, kTargetSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
nullptr);
|
||||||
|
glGenFramebuffers(1, &m_fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw once with `expected` pushed to the shader and report the centre pixel.
|
||||||
|
void DrawAndReadCentre(int expectedWidth, int expectedHeight, unsigned char (¢re)[4]) {
|
||||||
|
const GLint location = glGetUniformLocation(m_program, "g_expected_size");
|
||||||
|
ASSERT_NE(location, -1) << "the program has no g_expected_size uniform";
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glUniform2i(location, expectedWidth, expectedHeight);
|
||||||
|
glViewport(0, 0, kTargetSize, kTargetSize);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the draw left a GL error";
|
||||||
|
|
||||||
|
std::vector<unsigned char> pixels(static_cast<std::size_t>(kTargetSize) * kTargetSize * 4, 0);
|
||||||
|
glReadPixels(0, 0, kTargetSize, kTargetSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the target back errored";
|
||||||
|
const std::size_t offset =
|
||||||
|
(static_cast<std::size_t>(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
centre[i] = pixels[offset + static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_fbo = 0;
|
||||||
|
GLuint m_color = 0;
|
||||||
|
GLuint m_image = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The whole conformance shape: bind once, draw, re-specify the SAME texture smaller, draw
|
||||||
|
// again. The first draw is the control - it proves the binding and the shader work at all -
|
||||||
|
// and the second is the regression pin. Blue would mean the draw never ran; red means the
|
||||||
|
// image unit answered with the size the texture had BEFORE the re-spec.
|
||||||
|
TEST_F(ImageSizeAfterRespecScenario, ADrawSeesTheNewSizeOfARespecifiedImageTexture) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!FragmentImagesAreUsable()) GTEST_SKIP() << "no fragment-stage image uniform available";
|
||||||
|
|
||||||
|
m_program = MakeProgram();
|
||||||
|
if (m_program == 0) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
MakeRenderTarget();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "setting the render target up errored";
|
||||||
|
|
||||||
|
glGenTextures(1, &m_image);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_image);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glBindImageTexture(0, m_image, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "binding the image texture errored";
|
||||||
|
|
||||||
|
unsigned char centre[4] = {0, 0, 0, 0};
|
||||||
|
DrawAndReadCentre(32, 32, centre);
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got ("
|
||||||
|
<< static_cast<int>(centre[0]) << ", "
|
||||||
|
<< static_cast<int>(centre[1]) << ", "
|
||||||
|
<< static_cast<int>(centre[2]) << ")";
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
||||||
|
|
||||||
|
// The re-spec. The image unit is deliberately NOT re-bound: GL 4.6 core 8.26 says the
|
||||||
|
// unit references the texture object, so this alone has to be visible to the next draw.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_image);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the image texture errored";
|
||||||
|
|
||||||
|
DrawAndReadCentre(16, 16, centre);
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[0]), 0)
|
||||||
|
<< "after the re-spec the draw still sees the OLD image size; centre pixel was ("
|
||||||
|
<< static_cast<int>(centre[0]) << ", " << static_cast<int>(centre[1]) << ", "
|
||||||
|
<< static_cast<int>(centre[2]) << ")";
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user