mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
246
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4826806881 | ||
|
|
e7a6a72f6a | ||
|
|
62a7786184 | ||
|
|
ef6227e19b | ||
|
|
9bd6d39403 | ||
|
|
6b681c4a63 | ||
|
|
80a6b39003 | ||
|
|
97b997d5da | ||
|
|
7d80c9678e | ||
|
|
72aa9191b4 | ||
|
|
1e3a74686f | ||
|
|
0fe7bf82d2 | ||
|
|
416cd23c28 | ||
|
|
5f8e8db1b9 | ||
|
|
bdf05514c3 | ||
|
|
bf8b39a867 | ||
|
|
d4504e30f8 | ||
|
|
9087f13308 | ||
|
|
44ffafb2dc | ||
|
|
12b57055b7 | ||
|
|
d0ff647581 | ||
|
|
30d72c5b4e | ||
|
|
d9f4698d98 | ||
|
|
878db2c405 | ||
|
|
77ecde1524 | ||
|
|
510ecd9293 | ||
|
|
440d3c5253 | ||
|
|
83b16561c2 | ||
|
|
275dd3edb4 | ||
|
|
a196ada4c1 | ||
|
|
3aa4d8af1f | ||
|
|
bf86b1ede6 | ||
|
|
087685d19b | ||
|
|
5635e33ffe | ||
|
|
5d99ee435f | ||
|
|
b566bf4db9 | ||
|
|
09d2bb11b7 | ||
|
|
fee3902472 | ||
|
|
da249f30e2 | ||
|
|
8566a288f8 | ||
|
|
2318f6ae44 | ||
|
|
fe3dc1dde8 | ||
|
|
6672778b80 | ||
|
|
6e0e3df372 | ||
|
|
bee22f9d26 | ||
|
|
8952b14024 | ||
|
|
458ccde176 | ||
|
|
901d48a678 | ||
|
|
e8ee7b1a88 | ||
|
|
1154f9a00d | ||
|
|
7ef7c7e543 | ||
|
|
6c7ad0a1bf | ||
|
|
38d4c2372c | ||
|
|
8a239177ac | ||
|
|
87ee17c68c | ||
|
|
aa005720d0 | ||
|
|
c1a7ffac94 | ||
|
|
bdd4bed431 | ||
|
|
10315e71f3 | ||
|
|
bfa087d0f7 | ||
|
|
a1e22c26ab | ||
|
|
bd2b4158e0 | ||
|
|
9c7339b214 | ||
|
|
50815a232e | ||
|
|
d380a01f32 | ||
|
|
7566a0b002 | ||
|
|
42e0f47ebb | ||
|
|
9bbf71990c | ||
|
|
2f8d0f0d51 | ||
|
|
3363258908 | ||
|
|
9c773182bb | ||
|
|
8349babe90 | ||
|
|
1794ac94b1 | ||
|
|
8b31de2f8d | ||
|
|
50fb13430f | ||
|
|
d4f8adcf6d | ||
|
|
795e08f7e6 | ||
|
|
1e7ecab4db | ||
|
|
81b17c0b75 | ||
|
|
d1edf765f5 | ||
|
|
97e07190ac | ||
|
|
a4dcdf989e | ||
|
|
1c113e4b26 | ||
|
|
bf9cfb3079 | ||
|
|
e1818d497a | ||
|
|
d7f66722d1 | ||
|
|
92dc41ebf9 | ||
|
|
feea131d8b | ||
|
|
19f4402fbf | ||
|
|
1350031368 | ||
|
|
0ee3384b22 | ||
|
|
ba3f8d6774 | ||
|
|
3327784fd0 | ||
|
|
ff426da3a9 | ||
|
|
08419a1fe6 | ||
|
|
5d51372c44 | ||
|
|
7fd4550968 | ||
|
|
971537058e | ||
|
|
dd98c450ad | ||
|
|
5dbbbbd7eb | ||
|
|
5d140a41ce | ||
|
|
bf376b230f | ||
|
|
29599dcf90 | ||
|
|
734fab9f90 | ||
|
|
2cd1809c29 | ||
|
|
faed498476 | ||
|
|
8282eecbfa | ||
|
|
f4f3afb0b6 | ||
|
|
a28da07641 | ||
|
|
200c21336f | ||
|
|
2c3fc583d5 | ||
|
|
645a12d8bc | ||
|
|
8e6acc5528 | ||
|
|
525ffe0f14 | ||
|
|
ad28d2b744 | ||
|
|
eab622388f | ||
|
|
75e573c923 | ||
|
|
0d0ef13619 | ||
|
|
23565fcacd | ||
|
|
5dc26e3e2c | ||
|
|
90564aa82e | ||
|
|
7520607d47 | ||
|
|
57635a9198 | ||
|
|
c19d0f0b75 | ||
|
|
e42e7d00f5 | ||
|
|
62695ee3c2 | ||
|
|
02cc0ce83c | ||
|
|
9e52a0b23e | ||
|
|
9dee53337f | ||
|
|
28c5badf8f | ||
|
|
01116f7b41 | ||
|
|
05d627ba2d | ||
|
|
ea5d52f126 | ||
|
|
3c70b4fc0f | ||
|
|
b6d6316333 | ||
|
|
3c9ab5a68f | ||
|
|
532b5e9cc5 | ||
|
|
06605ed0ea | ||
|
|
e1d5bdc4a5 | ||
|
|
66867a41ba | ||
|
|
ebff4b21f7 | ||
|
|
d4e7378868 | ||
|
|
01d20e5c96 | ||
|
|
b04c67d9a8 | ||
|
|
685d83e3ec | ||
|
|
009b140691 | ||
|
|
1f44e5bc1d | ||
|
|
d9aebcba26 | ||
|
|
0db666897e | ||
|
|
5f445e499f | ||
|
|
c52ebd5bf6 | ||
|
|
8e072bc793 | ||
|
|
88ee75be0e | ||
|
|
0dbb4ceba8 | ||
|
|
a94b3e0bd5 | ||
|
|
764b6e044d | ||
|
|
c1d89de729 | ||
|
|
c136384f97 | ||
|
|
1920a3d16f | ||
|
|
11f4b4bd3b | ||
|
|
9ef33f4274 | ||
|
|
e430e1b3be | ||
|
|
e315d9e798 | ||
|
|
6f299372c6 | ||
|
|
be7bf21eb8 | ||
|
|
0e4302b399 | ||
|
|
c9c2dcb42a | ||
|
|
2d938971b9 | ||
|
|
f7e23d5d83 | ||
|
|
02fbb816e9 | ||
|
|
0e0882cfc6 | ||
|
|
de09646d5e | ||
|
|
747864777e | ||
|
|
6fc3504bd9 | ||
|
|
df1bcdba09 | ||
|
|
843c61dee1 | ||
|
|
c0a3f4cc50 | ||
|
|
06744fde7f | ||
|
|
6cc9faf772 | ||
|
|
7168f2ef77 | ||
|
|
07669aacd4 | ||
|
|
d52a3b2196 | ||
|
|
9e23016dd4 | ||
|
|
f1354dc25e | ||
|
|
b7a694711a | ||
|
|
2ae848ca19 | ||
|
|
acf86d1fb6 | ||
|
|
07a0408a28 | ||
|
|
8cf2e2aea9 | ||
|
|
31252cf0da | ||
|
|
2635fe84b6 | ||
|
|
e3163233a5 | ||
|
|
eb9e4fdac1 | ||
|
|
90b7a689c5 | ||
|
|
e69e939d1a | ||
|
|
b675e2a0b0 | ||
|
|
6979926a6f | ||
|
|
d5286e69b6 | ||
|
|
0f2fcbc469 | ||
|
|
18fccdd796 | ||
|
|
0d2fceab0e | ||
|
|
82decded58 | ||
|
|
d04a3394de | ||
|
|
33eabfc2ff | ||
|
|
56e5d13dc9 | ||
|
|
34b7cc772f | ||
|
|
c1d6a3c908 | ||
|
|
386bd7e461 | ||
|
|
12df061e0b | ||
|
|
d83a48da5c | ||
|
|
b3100b0de5 | ||
|
|
1cde801a01 | ||
|
|
52c050131e | ||
|
|
ebb8a4cebf | ||
|
|
5398fb4289 | ||
|
|
7f2ca68615 | ||
|
|
b12ef4d717 | ||
|
|
724755d9df | ||
|
|
59f7059bf4 | ||
|
|
4446c861be | ||
|
|
602da1d131 | ||
|
|
06bbaf32b1 | ||
|
|
9bcf0a15a0 | ||
|
|
26e5a946ac | ||
|
|
dc543fa905 | ||
|
|
f559d68728 | ||
|
|
48968a663f | ||
|
|
0fdcb5d6c4 | ||
|
|
6b25e7a7e3 | ||
|
|
50d260c840 | ||
|
|
6a8bf4c03c | ||
|
|
01098e9dd7 | ||
|
|
82244e9048 | ||
|
|
7bd2f08313 | ||
|
|
121b99f8c3 | ||
|
|
e8d79344d6 | ||
|
|
88be35c9ba | ||
|
|
60808b6cf2 | ||
|
|
eb2f14e55a | ||
|
|
0eb5d54bb8 | ||
|
|
6a2e9dc791 | ||
|
|
d5aceebd7b | ||
|
|
473d9951b7 | ||
|
|
13783e3aec | ||
|
|
6bb844b1c1 | ||
|
|
6162603072 |
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
|
||||
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
|
||||
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
||||
|
||||
@@ -6,6 +6,10 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -417,12 +421,12 @@ jobs:
|
||||
|
||||
- name: Retrace and validate
|
||||
env:
|
||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
run: |
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
|
||||
+784
-9
@@ -6,7 +6,19 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
baseline_sha:
|
||||
description: >-
|
||||
The commit monolith-symbol-report compares this tree against. P1's G1 says the pull
|
||||
build is byte-identical to feat/disaggregated@087685d1, and that is what the default
|
||||
names. The trigger set is unchanged: this job runs on workflow_dispatch only.
|
||||
required: false
|
||||
default: "087685d1"
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -265,16 +277,26 @@ jobs:
|
||||
# crash stack without burning a CI round on an in-workflow debugger.
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
|
||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
# Second, filtered pass: with the range-invalidating map flush disabled,
|
||||
# the buffer scenarios run on the upload ring's staged-copy tier - which
|
||||
# the default pass never reaches (the map tier absorbs every flush on
|
||||
# Mesa), so without this the Mali fallback tier would have zero CI
|
||||
# coverage. The flag is NOT baked into the ctest ENVIRONMENT properties,
|
||||
# so an inline env reaches the test processes (unlike the ICD pin above).
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L integration-gpu --no-tests=error
|
||||
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest -V -L integration-gpu \
|
||||
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
||||
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --output-on-failure -L integration-gpu \
|
||||
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
@@ -285,6 +307,399 @@ jobs:
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
# THE THIRD CI MODE (ARCHITECTURE.md 13.2-(2)): the same library, built with the PipeInputs
|
||||
# comparator compiled in, running the integration suite and a trace subset with two state models
|
||||
# in one address space. It is a second build rather than a flag on the first because
|
||||
# MOBILEGL_PIPE_VERIFY is a compile-time option - the snapshot, the entry compare and the
|
||||
# compare-at-read hook do not exist in the shipped library, and are never meant to.
|
||||
build-linux-verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
BUILD_DIR: build-verify
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
CCACHE_COMPRESS: "true"
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 32
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||
|
||||
- name: Prepare Vulkan SDK
|
||||
uses: humbletim/setup-vulkan-sdk@v1.2.1
|
||||
with:
|
||||
vulkan-query-version: 1.4.304.1
|
||||
vulkan-components: Vulkan-Headers, Vulkan-Loader
|
||||
vulkan-use-cache: true
|
||||
|
||||
- name: Update glslang external sources
|
||||
working-directory: 3rdparty/glslang
|
||||
run: python update_glslang_sources.py
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
|
||||
|
||||
- name: Show installed toolchain
|
||||
run: |
|
||||
ccache --version
|
||||
clang-20 --version
|
||||
clang++-20 --version
|
||||
ld.lld-20 --version || ld.lld --version || true
|
||||
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
|
||||
|
||||
- name: Configure CMake
|
||||
# Release/INFO like the shipped build on purpose. The poison arms in this configuration
|
||||
# through MOBILEGL_PIPE_VERIFY (PipeInputs.h derives MOBILEGL_PIPE_POISON from it), so this
|
||||
# job needs neither a Debug log level nor MOBILEGL_BUILD_DISAGGREGATED - and a Debug build
|
||||
# would compare a different library from the one the other lanes measure. (build-linux
|
||||
# switches to Debug under ACTIONS_STEP_DEBUG; this job deliberately does not - a Debug
|
||||
# build flips CXX_VISIBILITY_PRESET and arms MOBILEGL_PIPE_POISON through a second, unrelated
|
||||
# arm of its #if, so the debug switch would change what the lane is measuring.)
|
||||
run: |
|
||||
cmake -S . -B "${BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_C_COMPILER=clang-20 \
|
||||
-DCMAKE_CXX_COMPILER=clang++-20 \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_TEST=ON \
|
||||
-DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
||||
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
|
||||
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||
-DMOBILEGL_PIPE_VERIFY=ON \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
|
||||
- name: Build
|
||||
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
|
||||
|
||||
# The lane is worthless if the option silently did not take, and that is a one-character
|
||||
# mistake away at all times (a typo'd -D is not an error in CMake). Two checks, both cheap:
|
||||
# the comparator's entry point must be in the library, and the fill entry point with it.
|
||||
#
|
||||
# `nm` and NOT `nm -D`. The library is built CXX_VISIBILITY_PRESET hidden in every non-Debug
|
||||
# configuration (CMakeLists.txt:600-604) and the MGPipe entry points are plain namespace
|
||||
# functions with no export attribute, so not one of them appears in the DYNAMIC table: on a
|
||||
# perfectly healthy verify build `nm -D --defined-only ... | grep -c MGPipe` answers 0 out of
|
||||
# ~11900 exported symbols, and a gate spelled that way is red forever for a reason that has
|
||||
# nothing to do with what it claims to test. The static symbol table has them as local `t`
|
||||
# entries, this artifact is never stripped, and `No MG_Remote in the pull build` below already
|
||||
# uses this spelling. The symbol count guards the remaining hole: a stripped library would
|
||||
# make both greps fail for a third, silent reason.
|
||||
- name: The verify library really carries the comparator
|
||||
run: |
|
||||
test -f "${BUILD_DIR}/libMobileGL.so"
|
||||
defined=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | wc -l)
|
||||
if [ "${defined}" -lt 1000 ]; then
|
||||
echo "::error::nm --defined-only sees only ${defined} symbols in ${BUILD_DIR}/libMobileGL.so - it looks stripped, so the two checks below could not have failed honestly"
|
||||
exit 1
|
||||
fi
|
||||
for entry in MGPipeVerifyInputs MGPipeFillForVerb; do
|
||||
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -q "${entry}"; then
|
||||
echo "::error::libMobileGL.so defines no ${entry}: -DMOBILEGL_PIPE_VERIFY=ON did not take, and every lane that consumes this artifact would run the comparator-free library and pass having compared nothing"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "libMobileGL.so defines MGPipeVerifyInputs and MGPipeFillForVerb (${defined} defined symbols)"
|
||||
|
||||
- name: Show ccache stats
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
- name: Release superseded ccache entry
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
run: gh cache delete "${CACHE_KEY}" || true
|
||||
|
||||
- name: Save ccache
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
|
||||
- name: Package Linux verify runtime
|
||||
run: |
|
||||
mkdir -p ci-artifacts
|
||||
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
|
||||
tar \
|
||||
--exclude='*/CMakeFiles' \
|
||||
--exclude='*.o' \
|
||||
--exclude='*.a' \
|
||||
--exclude='*.ninja*' \
|
||||
--exclude='build.ninja' \
|
||||
--exclude='cmake_install.cmake' \
|
||||
-czf ci-artifacts/mobilegl-linux-runtime-verify.tgz \
|
||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
||||
"${SHARED_LIBS[@]}"
|
||||
|
||||
- name: Upload Linux verify runtime
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mobilegl-linux-runtime-verify
|
||||
path: ci-artifacts/mobilegl-linux-runtime-verify.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
# The verify lane itself, plus the two negative controls that keep it falsifiable. The controls
|
||||
# are ALWAYS-ON steps, not a manual exercise: a gate that can only be shown to work by someone
|
||||
# remembering to break it on purpose is a gate that has already stopped working.
|
||||
integration-verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
needs: build-linux-verify
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Install runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||
|
||||
- name: Download Linux verify runtime
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mobilegl-linux-runtime-verify
|
||||
path: .
|
||||
|
||||
- name: Unpack Linux verify runtime
|
||||
run: |
|
||||
tar -xzf mobilegl-linux-runtime-verify.tgz
|
||||
test -f build-verify/libMobileGL.so
|
||||
|
||||
- name: Normalize CTest command paths
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
for path in Path('build-verify').rglob('CTestTestfile.cmake'):
|
||||
text = path.read_text()
|
||||
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
|
||||
path.write_text(text)
|
||||
PY
|
||||
|
||||
- name: Integration scenarios under MOBILEGL_PIPE_VERIFY
|
||||
working-directory: build-verify
|
||||
# --no-tests=error is half the gate: the verify entries only exist when the library was
|
||||
# configured with -DMOBILEGL_PIPE_VERIFY=ON, so a build that lost the option matches no
|
||||
# tests and reds here instead of reporting a green run of nothing. The other half is
|
||||
# PipeVerifyArmingScenario.Armed, which fails when the library never printed its arming
|
||||
# line - the failure mode a bare `MOBILEGL_PIPE_VERIFY=1` cannot detect by itself.
|
||||
#
|
||||
# SCOPE, stated so nobody reads more into a green than is there: this is every integration
|
||||
# ENTRY under the comparator, not every integration CONFIGURATION. The `integration` job
|
||||
# runs a second, filtered pass with MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 for the
|
||||
# upload ring's staged-copy tier; that pass is 186 entries here and, at the 5-10x the
|
||||
# comparator costs, is not affordable inside this job's budget. The tier is covered by
|
||||
# `integration`, unverified, and P2 can take it once the comparator's cost is known.
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L integration-verify --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L integration-verify --no-tests=error
|
||||
fi
|
||||
|
||||
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
|
||||
# library opens it fopen(path, "w"), so after an ambient lane of 400-odd processes the file
|
||||
# holds the LAST one - grepping it would say nothing about the other 405 and would red a
|
||||
# healthy lane whenever the last entry happened not to issue a verb (which is what the
|
||||
# PoisonOmissionScenario parent, the last ambient entry, does by construction: it forks,
|
||||
# execve()s and reads files). The DirectGLES.VerifyArming. / DirectVulkan.VerifyArming.
|
||||
# entries are one process each on a log path nothing else writes, so this grep means exactly
|
||||
# what it says.
|
||||
#
|
||||
# What it proves: arming is a property of (this library, this environment), and these two
|
||||
# processes ran the same library with the same MOBILEGL_PIPE_VERIFY=1 as their ~400 ambient
|
||||
# siblings. It is not, and cannot be, a per-process census - the shared log cannot support one.
|
||||
# It catches the case ctest cannot: an arming entry that SKIPPED still reports green.
|
||||
- name: The verify lanes armed the comparator
|
||||
working-directory: build-verify
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
logs=(MobileGL/MG_IntegrationTest/pipe-verify-arming-*.log)
|
||||
if [ ${#logs[@]} -lt 2 ]; then
|
||||
echo "::error::found ${#logs[@]} pipe-verify-arming-*.log (expected one per backend). The VerifyArming. entries did not run, so nothing in this job establishes that the comparator was ever armed."
|
||||
exit 1
|
||||
fi
|
||||
for log in "${logs[@]}"; do
|
||||
if ! grep -q "MGPipe: verify armed" "${log}"; then
|
||||
echo "::error::${log} carries no arming line: that lane's process ran the whole scenario without the comparator, so every green entry beside it is green for no reason"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "arming line present in all ${#logs[@]} arming-lane log(s)"
|
||||
|
||||
# NEGATIVE CONTROL A (gate G4). The knob perturbs one field in the snapshot arm before the
|
||||
# entry compare, so a working comparator must abort the run. This step passes when ctest
|
||||
# FAILS - `if ctest ...; then error` - which is the only shape that can catch a comparator
|
||||
# that silently compares nothing.
|
||||
#
|
||||
# The knob reaches the test process through the JOB environment: no ctest ENVIRONMENT
|
||||
# property on the ambient Verify. entries names it (MG_IntegrationTest/CMakeLists.txt says
|
||||
# so out loud), and a property entry would otherwise override this and the control would
|
||||
# prove nothing. Same precedent as MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH in `integration`.
|
||||
- name: Negative control A - a corrupted snapshot field must turn the lane red
|
||||
working-directory: build-verify
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_PIPE_VERIFY_CORRUPT: GetRenderStateParameters
|
||||
run: |
|
||||
FILTER='DirectGLES\.Verify\..*ClearThenReadPixels'
|
||||
# An empty selection would ALSO make ctest exit non-zero (--no-tests=error), and this
|
||||
# step reads non-zero as "the control worked" - so the selection is counted first. A
|
||||
# control that passes because it ran nothing is worse than no control.
|
||||
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
|
||||
if [ "${matched}" -lt 1 ]; then
|
||||
echo "::error::negative control A selected ${matched} tests; its filter no longer matches anything"
|
||||
exit 1
|
||||
fi
|
||||
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
|
||||
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left ${matched} verify entries GREEN. The comparator is not comparing, so every green entry above is green for no reason."
|
||||
exit 1
|
||||
fi
|
||||
echo "the corrupted field turned ${matched} selected entries red, as it must"
|
||||
|
||||
# NEGATIVE CONTROL B (gate G5). The omission skips the STAMP of one field for one verb while
|
||||
# still copying its value - indistinguishable from a fill row nobody wrote - so the poison
|
||||
# must abort the glGenerateMipmap. Again: this step passes when ctest fails.
|
||||
#
|
||||
# The entry it targets is PoisonOmissionScenario.WithoutOmissionCompletes, which is green in
|
||||
# the ambient lane above and is the ONLY integration entry in the tree that calls
|
||||
# glGenerateMipmap at all. It deliberately does not skip itself when the knob is set, exactly
|
||||
# so that this control has something to turn red.
|
||||
- name: Negative control B - an omitted fill point must turn the lane red on that verb
|
||||
working-directory: build-verify
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_PIPE_POISON_OMIT: GenerateMipmap:GetActiveTextureUnit
|
||||
run: |
|
||||
FILTER='DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes'
|
||||
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
|
||||
if [ "${matched}" -lt 1 ]; then
|
||||
echo "::error::negative control B selected ${matched} tests; its filter no longer matches anything"
|
||||
exit 1
|
||||
fi
|
||||
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
|
||||
echo "::error::MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit left the verify lane GREEN. The per-verb poison is not armed, so a forgotten fill row would ship silently."
|
||||
exit 1
|
||||
fi
|
||||
echo "the omitted fill point turned the lane red, as it must"
|
||||
|
||||
- name: Upload verify lane logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: integration-verify-logs
|
||||
path: build-verify/MobileGL/MG_IntegrationTest/pipe-*.log*
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: integration-verify-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
|
||||
# flatc is deliberately absent from the default build graph (a codegen step in
|
||||
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc
|
||||
# and trying to run it on the host). This job is what keeps the committed
|
||||
# header honest: build the pinned flatc, regenerate, and fail on any diff.
|
||||
# It needs no MobileGL build, so it does not depend on build-linux.
|
||||
flatc-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Check out the FlatBuffers submodule only
|
||||
# Just this one: the schema check has nothing to do with glslang,
|
||||
# SPIRV-Cross or the trace fixtures.
|
||||
run: git submodule update --init 3rdparty/flatbuffers
|
||||
|
||||
- name: Regenerate protocol_generated.h
|
||||
run: python3 scripts/gen_protocol.py --build-dir "${{ runner.temp }}/flatc-build"
|
||||
|
||||
- name: Fail if the committed header is stale
|
||||
run: git diff --exit-code -- MobileGL/MG_Remote/Protocol/generated/protocol_generated.h
|
||||
|
||||
# P0.5 interface-purity gate A (ARCHITECTURE.md:501): the two extracted headers' include closure,
|
||||
# asserted on `-H` output because `nm --undefined-only` is blind to "included but not called" -
|
||||
# a header whose types are never named leaves no symbol behind, and "included at all" is exactly
|
||||
# the coupling P1 and P7 have to sever. Needs a preprocessor and three header submodules, no
|
||||
# CMake configure and no glslang sources, so like pipe-gates it does not depend on build-linux.
|
||||
# The script's own --self-test is always on: a negative control that stopped tripping fails the
|
||||
# job, because a gate that cannot go red is not a gate (ROADMAP.md:7).
|
||||
include-graph-check:
|
||||
name: Include-closure purity gate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check out the three header submodules the closure needs
|
||||
# ska/flat_hash_map.hpp, xxhash.h and vulkan/vulkan.h are the only submodule headers
|
||||
# Includes.h reaches; glslang and spirv_cross are vendored under include/.
|
||||
run: git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers
|
||||
|
||||
- name: Install clang and the X11 headers vulkan.h pulls on Linux
|
||||
# Includes.h defines VK_USE_PLATFORM_XLIB_KHR before <vulkan/vulkan.h>, which then
|
||||
# includes <X11/Xlib.h>; without libx11-dev every clang-mode probe dies in the
|
||||
# preprocessor and the gate reports 5 problems that have nothing to do with purity.
|
||||
run: sudo apt-get update && sudo apt-get install -y clang-20 libx11-dev
|
||||
|
||||
- name: Include-closure assertions and negative control
|
||||
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
|
||||
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-linux
|
||||
@@ -496,6 +911,7 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
verify-matrix: ${{ steps.trace-cases.outputs.verify-matrix }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -505,6 +921,9 @@ jobs:
|
||||
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"
|
||||
# The subset the verify build retraces ("verify": true in trace_cases.json). It is a
|
||||
# SUBSET of the matrix above, so retrace-verify needs no fixtures of its own.
|
||||
echo "verify-matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-verify-matrix)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
name: trace fixture (${{ matrix.case }})
|
||||
@@ -644,9 +1063,9 @@ jobs:
|
||||
fi
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
||||
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
||||
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
||||
export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||
export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1
|
||||
export MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1
|
||||
fi
|
||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
||||
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||
@@ -718,10 +1137,295 @@ jobs:
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
# The trace half of the third CI mode. Same replay, same goldens, but the library underneath is
|
||||
# the verify build and MOBILEGL_PIPE_VERIFY=1 is in the environment, so every backend read of
|
||||
# frontend state is checked against a snapshot taken at the verb boundary. Eight cases rather
|
||||
# than the full lane's 40 (tools/trace_replay/trace_cases.json, "verify": true): the comparator
|
||||
# is budgeted at 5-10x, and the full sweep is a phase-exit / workflow_dispatch run.
|
||||
retrace-verify:
|
||||
name: retrace verify (${{ matrix.backend }}, ${{ matrix.case }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
needs:
|
||||
- build-linux-verify
|
||||
- build-retrace
|
||||
- trace-cases
|
||||
- trace-fixtures
|
||||
if: ${{ always() && needs.build-linux-verify.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.verify-matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 16
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download trace fixture
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: trace-fixture-${{ matrix.case }}
|
||||
path: trace-fixture-download
|
||||
|
||||
- name: Install trace fixture
|
||||
run: |
|
||||
mkdir -p tools/trace_replay/fixtures
|
||||
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Install runtime dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvulkan1 libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers
|
||||
test -e /usr/lib/x86_64-linux-gnu/libEGL.so
|
||||
test -e /usr/lib/x86_64-linux-gnu/libGLESv2.so
|
||||
|
||||
- name: Download Linux verify runtime
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mobilegl-linux-runtime-verify
|
||||
path: .
|
||||
|
||||
- name: Download trace replay
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mobilegl-trace-replay
|
||||
path: .
|
||||
|
||||
- name: Unpack the VERIFY runtime as the library under test
|
||||
# build-retrace's CTestTestfile.cmake has the absolute path
|
||||
# <workspace>/build-linux/libMobileGL.so frozen into every case, so the swap happens here
|
||||
# rather than through a variable: the verify .so is put where that path points. The nm
|
||||
# check is what makes the swap falsifiable - a run against the ordinary library would
|
||||
# carry no comparator, ignore MOBILEGL_PIPE_VERIFY entirely, and match its golden.
|
||||
#
|
||||
# `nm`, not `nm -D`, for the reason spelled out in build-linux-verify: everything MGPipe is
|
||||
# hidden-visibility in a Release build and the dynamic table has none of it.
|
||||
run: |
|
||||
tar -xzf mobilegl-linux-runtime-verify.tgz
|
||||
tar -xzf mobilegl-trace-replay.tgz
|
||||
test -f build-verify/libMobileGL.so
|
||||
test -f build-retrace/tools/trace_replay/mobilegl_trace_replay
|
||||
mkdir -p build-linux
|
||||
cp build-verify/libMobileGL.so build-linux/libMobileGL.so
|
||||
if ! nm --defined-only build-linux/libMobileGL.so | grep -q MGPipeVerifyInputs; then
|
||||
echo "::error::the library unpacked at build-linux/libMobileGL.so defines no MGPipeVerifyInputs, so this retrace would replay against a comparator-free build and pass on its golden having verified nothing"
|
||||
exit 1
|
||||
fi
|
||||
echo "the library at build-linux/libMobileGL.so is the verify build"
|
||||
|
||||
- name: Retrace and validate under MOBILEGL_PIPE_VERIFY
|
||||
working-directory: build-retrace/tools/trace_replay
|
||||
# run_trace_case.cmake turns MOBILEGL_PIPE_VERIFY into three assertions of its own (the
|
||||
# arming line, no Fatal{PipeVerifyDiffer, no Fatal{UnmigratedPipeInput), so a case that
|
||||
# somehow ran the wrong library reds here instead of passing on its golden.
|
||||
# --timeout 10800: the 1800s cases run 5-10x slower with both comparator arms live, which
|
||||
# is well past ctest's 1500s default.
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
export MOBILEGL_PIPE_VERIFY=1
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||
fi
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
|
||||
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
|
||||
fi
|
||||
ctest -V --no-tests=error --timeout 10800 \
|
||||
-R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||
|
||||
# The retrace lane's own always-on negative control, on one case so it costs one short trace:
|
||||
# with a snapshot field corrupted, the SAME replay must fail. Without it, "40 traces, zero
|
||||
# divergences" would be a statement about a comparator nobody watched.
|
||||
#
|
||||
# The rerun replays into the SAME case directory, so the verified run's images are put aside
|
||||
# first and restored before the verdict: "Upload actual image" below runs `if: always()` and
|
||||
# would otherwise ship the deliberately corrupted run's output under the name of the good one.
|
||||
# The restore happens whichever way the control goes, which is why the ctest exit status is
|
||||
# captured rather than tested inline.
|
||||
- name: Negative control - a corrupted snapshot field must red this retrace
|
||||
if: ${{ matrix.case == 'OpenRA' && matrix.backend == 'DirectGLES' }}
|
||||
working-directory: build-retrace/tools/trace_replay
|
||||
run: |
|
||||
export MOBILEGL_PIPE_VERIFY=1
|
||||
export MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters
|
||||
GOOD_OUTPUT="${RUNNER_TEMP}/openra-verified-output"
|
||||
rm -rf "${GOOD_OUTPUT}"
|
||||
if [ -d OpenRA ]; then
|
||||
cp -a OpenRA "${GOOD_OUTPUT}"
|
||||
fi
|
||||
set +e
|
||||
ctest -V --no-tests=error --timeout 10800 \
|
||||
-R '^MobileGLTraceReplay\.OpenRA\.DirectGLES$'
|
||||
control_rc=$?
|
||||
set -e
|
||||
if [ -d "${GOOD_OUTPUT}" ]; then
|
||||
rm -rf OpenRA
|
||||
mv "${GOOD_OUTPUT}" OpenRA
|
||||
echo "restored the verified run's OpenRA output over the corrupted rerun's"
|
||||
fi
|
||||
if [ "${control_rc}" -eq 0 ]; then
|
||||
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left the OpenRA retrace GREEN, so the comparator is not comparing and the whole verify retrace lane proves nothing."
|
||||
exit 1
|
||||
fi
|
||||
echo "the corrupted field turned the retrace red, as it must (ctest exit ${control_rc})"
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: retrace-verify-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload actual image
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: retrace-verify-result-${{ matrix.backend }}-${{ matrix.case }}
|
||||
path: |
|
||||
build-retrace/tools/trace_replay/${{ matrix.case }}/actual-images/**
|
||||
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
|
||||
if-no-files-found: warn
|
||||
|
||||
# G1's own job: the pull build must be the tree before P1, symbol for symbol and byte for byte.
|
||||
# workflow_dispatch only - it builds the library twice from scratch, and its answer is about a
|
||||
# BASELINE rather than about this push, so a per-push run would be measuring the wrong pair.
|
||||
monolith-symbol-report:
|
||||
name: monolith symbol report
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
CCACHE_COMPRESS: "true"
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 32
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||
|
||||
- name: Prepare Vulkan SDK
|
||||
uses: humbletim/setup-vulkan-sdk@v1.2.1
|
||||
with:
|
||||
vulkan-query-version: 1.4.304.1
|
||||
vulkan-components: Vulkan-Headers, Vulkan-Loader
|
||||
vulkan-use-cache: true
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build binutils
|
||||
|
||||
# Both sides with IDENTICAL flags, LTO off, the same compiler and the same standard library:
|
||||
# symbol_report.py's guard rails (scripts/symbol_report.py) say a mismatched pair "adds"
|
||||
# thousands of symbols and the comparison then means nothing. The library alone - no tests,
|
||||
# no benchmark, no integration test, no trace replay - because those targets do not ship.
|
||||
- name: Build the baseline library (${{ inputs.baseline_sha }})
|
||||
run: |
|
||||
git worktree add ../baseline "${{ inputs.baseline_sha }}"
|
||||
cd ../baseline
|
||||
git submodule update --init --recursive
|
||||
(cd 3rdparty/glslang && python update_glslang_sources.py)
|
||||
cmake -S . -B build-sym-base -G Ninja \
|
||||
-DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||
-DMOBILEGL_BUILD_DISAGGREGATED=OFF \
|
||||
-DMOBILEGL_PIPE_PUSH=OFF -DMOBILEGL_PIPE_VERIFY=OFF \
|
||||
-DMOBILEGL_ENABLE_LTO=OFF \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake --build build-sym-base --parallel "$(nproc)"
|
||||
cp build-sym-base/libMobileGL.so "${GITHUB_WORKSPACE}/libMobileGL-baseline.so"
|
||||
|
||||
- name: Build the head library
|
||||
run: |
|
||||
(cd 3rdparty/glslang && python update_glslang_sources.py)
|
||||
cmake -S . -B build-sym-head -G Ninja \
|
||||
-DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||
-DMOBILEGL_BUILD_DISAGGREGATED=OFF \
|
||||
-DMOBILEGL_PIPE_PUSH=OFF -DMOBILEGL_PIPE_VERIFY=OFF \
|
||||
-DMOBILEGL_ENABLE_LTO=OFF \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
cmake --build build-sym-head --parallel "$(nproc)"
|
||||
|
||||
# The monolith must not have grown a remote half. ARCHITECTURE.md:506: MG_Remote lives behind
|
||||
# MOBILEGL_BUILD_DISAGGREGATED and nothing of it may reach a shipped pull build.
|
||||
- name: No MG_Remote in the pull build
|
||||
run: |
|
||||
if nm --defined-only build-sym-head/libMobileGL.so | grep -q MG_Remote; then
|
||||
echo "::error::the pull build defines MG_Remote symbols; the disaggregated half leaked into the monolith"
|
||||
nm --defined-only build-sym-head/libMobileGL.so | grep MG_Remote | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "no MG_Remote symbols in the pull build"
|
||||
|
||||
- name: Symbol report (G1)
|
||||
run: |
|
||||
python3 scripts/symbol_report.py \
|
||||
--before libMobileGL-baseline.so \
|
||||
--after build-sym-head/libMobileGL.so \
|
||||
--threshold 0 \
|
||||
--fail-on-symbol-set-change \
|
||||
--fail-on-added-bytes 0 \
|
||||
--markdown symbol-report.md \
|
||||
--json symbol-report.json
|
||||
|
||||
- name: Upload the symbol report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: monolith-symbol-report
|
||||
path: |
|
||||
symbol-report.md
|
||||
symbol-report.json
|
||||
if-no-files-found: error
|
||||
|
||||
remove-artifact-clutter:
|
||||
name: remove artifact clutter
|
||||
runs-on: ubuntu-latest
|
||||
needs: retrace-summary
|
||||
# (d) retrace-verify too: this job deletes the trace-fixture-* artifacts, and the verify
|
||||
# retraces download the same ones.
|
||||
needs:
|
||||
- retrace-summary
|
||||
- retrace-verify
|
||||
if: always()
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -730,14 +1434,19 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Both retrace lanes, not just the pull one: `retrace verify (backend, case)` downloads
|
||||
# the same trace-fixture-<case> artifact, and a failed verify retrace is exactly when
|
||||
# someone needs that fixture to reproduce locally. The two prefixes are stripped in
|
||||
# order, longest first, because "retrace (" is not a prefix of "retrace verify (".
|
||||
declare -A failed_cases=()
|
||||
while IFS= read -r job_name; do
|
||||
case_name="${job_name#retrace (*, }"
|
||||
case_name="${job_name#retrace verify (*, }"
|
||||
case_name="${case_name#retrace (*, }"
|
||||
case_name="${case_name%)}"
|
||||
failed_cases["${case_name}"]=1
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
|
||||
--jq '.jobs[] | select((.name | startswith("retrace (")) or (.name | startswith("retrace verify ("))) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
|
||||
)
|
||||
|
||||
if ((${#failed_cases[@]})); then
|
||||
@@ -768,3 +1477,69 @@ jobs:
|
||||
)
|
||||
|
||||
echo "Deleted ${deleted} intermediate Linux artifact(s); retained ${retained} failed-retrace fixture(s)."
|
||||
|
||||
pipe-gates:
|
||||
name: MGPipe generators and hygiene gates
|
||||
runs-on: ubuntu-latest
|
||||
# Deliberately independent of build-linux: these are source-level gates, they take
|
||||
# seconds, and a broken build must not hide a drifted interface.
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
|
||||
# keeps the two interface tables, the wire records, the verify comparators, the
|
||||
# PipeInputs field ids, the read-inventory coverage and the render-state member list
|
||||
# from drifting apart from the catalogue. The generated files are committed
|
||||
# deliberately: the build must not depend on python.
|
||||
- name: Regenerate the MGPipe interface (G1-G7)
|
||||
run: |
|
||||
python3 scripts/gen_pipe.py
|
||||
git diff --exit-code -- MobileGL/MG_Pipe/generated
|
||||
|
||||
# The generators' own negative controls: canned inputs that MUST trip each structural check
|
||||
# (a field list that does not cover its struct's members, a verb set that is not the function
|
||||
# table's). Regenerating and diffing above cannot see a check that silently stopped
|
||||
# checking - a broken gate and a clean tree produce the same green.
|
||||
- name: The MGPipe generators' checks can still fail
|
||||
run: python3 scripts/gen_pipe.py --self-test
|
||||
|
||||
# The same question for the symbol tool the P1 gate is written in terms of.
|
||||
- name: The symbol report's buckets and gates can still fail
|
||||
run: python3 scripts/symbol_report.py --self-test
|
||||
|
||||
# Per-draw fprintf/printf instrumentation has repeatedly been committed by accident,
|
||||
# once inside a mutex critical section. Nothing under these two trees prints to a
|
||||
# stdio stream today - MGLOG_D compiles out in INFO builds and is the only channel
|
||||
# they are allowed to use - so this gate starts with no exceptions, and any addition
|
||||
# to it needs a reason in the pull request rather than a quiet whitelist entry. The
|
||||
# alternation names every stdio spelling, not just the two that were committed:
|
||||
# fprintf to either stream, printf, puts, and the iostream pair.
|
||||
- name: No stdio instrumentation in MG_Backend or MG_State
|
||||
run: |
|
||||
if grep -rnE 'fprintf[[:space:]]*\((stderr|stdout)|(^|[^[:alnum:]_>.])printf[[:space:]]*\(|(^|[^[:alnum:]_>.:])puts[[:space:]]*\(|std::(cout|cerr)' \
|
||||
MobileGL/MG_Backend MobileGL/MG_State; then
|
||||
echo "::error::stdio instrumentation found; use MGLOG_D (compiled out in INFO builds)"
|
||||
exit 1
|
||||
fi
|
||||
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
|
||||
|
||||
# Informational: the frontend mutation surface an MGPipe aggregate generation has to
|
||||
# cover. It becomes a gate in P2, when the mapping file exists to diff against
|
||||
# (ROADMAP.md:18 puts the first mapping round in P2, not P1).
|
||||
- name: MGPipe dirty-surface report
|
||||
run: python3 scripts/gen_pipe_dirty_surface.py --summary
|
||||
|
||||
# Warning only for now: the disaggregation documents are still being written, and a
|
||||
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
|
||||
# --strict when the documents settle.
|
||||
- name: Documentation citation lint
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
documents=(docs/Disaggregated/*.md)
|
||||
if [ ${#documents[@]} -eq 0 ]; then
|
||||
echo "no disaggregation documents to check"
|
||||
exit 0
|
||||
fi
|
||||
python3 scripts/check_doc_citations.py "${documents[@]}" || true
|
||||
|
||||
@@ -34,3 +34,6 @@
|
||||
[submodule "include/ska"]
|
||||
path = include/ska
|
||||
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||
[submodule "3rdparty/flatbuffers"]
|
||||
path = 3rdparty/flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
|
||||
+1
Submodule 3rdparty/flatbuffers added at 7e163021e5
Vendored
+1
-1
Submodule 3rdparty/glslang updated: fa562bb911...d89cf443bc
+141
@@ -14,6 +14,19 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
|
||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
|
||||
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
|
||||
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
|
||||
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
|
||||
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
|
||||
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||
|
||||
@@ -238,6 +251,8 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/PipeStats.cpp
|
||||
|
||||
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
|
||||
@@ -285,6 +300,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
@@ -306,6 +322,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -313,6 +330,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
@@ -335,6 +353,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
|
||||
@@ -392,6 +411,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
@@ -412,6 +432,65 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MG_Remote (disaggregated transport). Everything below is gated: with the
|
||||
# option OFF not one file here is compiled and no include path is added.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||
# a checkout without the submodule must configure and build, just without the
|
||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||
# lines later. Note this only checks for the RUNTIME headers - flatc is never
|
||||
# built here (see scripts/gen_protocol.py).
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
|
||||
message(WARNING
|
||||
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
|
||||
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
|
||||
"disaggregated shape for this configure; the cached ON takes effect once the "
|
||||
"submodule is present.")
|
||||
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
|
||||
# made the plain re-configure after `git submodule update` stay OFF with no message at
|
||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||
# was, so the next configure - with the submodule there - honours it.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
|
||||
# cache write, for the same reason as the disaggregated fallback above.
|
||||
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Transport/Ring.cpp
|
||||
MobileGL/MG_Remote/Transport/Doorbell.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegment.cpp
|
||||
# Both platform halves are listed unconditionally and each is empty on
|
||||
# the other OS, so neither can rot behind an `if (WIN32)` nobody
|
||||
# configures.
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
|
||||
MobileGL/MG_Remote/Transport/FdPassing.cpp
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -462,11 +541,26 @@ set(MOBILEGL_COMPILE_DEF
|
||||
-DASIO_NO_DEPRECATED
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/MobileGL
|
||||
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
|
||||
# line above too; this entry lets the client, the backends and MG_Remote spell them
|
||||
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
|
||||
${spirv-tools_SOURCE_DIR}
|
||||
${spirv-tools_SOURCE_DIR}/include
|
||||
${spirv-tools_BINARY_DIR}
|
||||
@@ -477,6 +571,13 @@ set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# Header-only runtime: an include path, no add_subdirectory, no link
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
@@ -693,3 +794,43 @@ endif()
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
|
||||
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
|
||||
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
|
||||
# so a second executable has to be built with an .so name and exec'd out of
|
||||
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
|
||||
# chain end to end: it is packaged like a library, exec'd from the app's own
|
||||
# untrusted_app process, and writes a marker the parent reads back.
|
||||
#
|
||||
# Off by default and ANDROID-only, so no shipping configuration builds it. The
|
||||
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
|
||||
# ---------------------------------------------------------------------------
|
||||
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
add_executable(MobileGLServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
|
||||
|
||||
# An executable that is named like a shared library still has to be a real
|
||||
# PIE executable: Android has refused non-PIE executables since API 21, and
|
||||
# the name alone does not change what the loader demands of the file.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServer"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServer PRIVATE -pie)
|
||||
|
||||
# AGP packages what the external native build drops into the per-ABI output
|
||||
# directory, and it selects by the .so extension. CMake puts executables in
|
||||
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
|
||||
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
|
||||
# the library directory when the generator gave us one.
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+172
-21
@@ -69,27 +69,34 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EsprytEnableTextureView = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool EsprytUseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool DisableSubgroup = false;
|
||||
Bool MagmaDisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// FixIterationRPSubgroupScratch below instead). Off by default.
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
@@ -99,12 +106,12 @@ namespace MobileGL::MG_Config {
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||
// verbatim.
|
||||
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||
Bool IterationRPFixBarrier = false;
|
||||
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
@@ -112,7 +119,7 @@ namespace MobileGL::MG_Config {
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
@@ -125,16 +132,39 @@ namespace MobileGL::MG_Config {
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool AvoidExplicitLodBias = false;
|
||||
Bool EsprytAvoidExplicitLodBias = false;
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
|
||||
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
|
||||
// match them by block name and member sequence instead. The Mali ES driver delivers
|
||||
// nothing at all through a located block once a tessellation or geometry stage is in
|
||||
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
|
||||
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
|
||||
// healthy driver - which is what the integration lane does, since llvmpipe and
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -144,10 +174,38 @@ namespace MobileGL::MG_Config {
|
||||
Bool CoherentAsFlush = false;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
Bool TraceSkipAutodestroy = false;
|
||||
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
Bool EsprytDisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
// glTexSubImage from the client pointer instead of staging them through the
|
||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||
// hatch).
|
||||
Bool EsprytDisableUnpackRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
|
||||
// (glBufferSubData / map flushes) back to the immediate driver upload instead
|
||||
// of queueing them for the staged-copy flush through the persistent-mapped
|
||||
// upload ring (negative control / driver-bug escape hatch; the immediate
|
||||
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
|
||||
Bool EsprytDisableUploadRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
|
||||
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
|
||||
// to the upload ring's staged glCopyBufferSubData (negative control / escape
|
||||
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
|
||||
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
@@ -168,10 +226,10 @@ namespace MobileGL::MG_Config {
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
Bool MagmaDisableRobustBufferAccess = false;
|
||||
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
@@ -212,7 +270,7 @@ namespace MobileGL::MG_Config {
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
@@ -224,7 +282,100 @@ namespace MobileGL::MG_Config {
|
||||
// the pre-emulation path, extension passthrough where it exists and
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
||||
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
|
||||
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
|
||||
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
|
||||
// packed16 allocations with a MIRRORED field order (allocation-scoped and
|
||||
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
|
||||
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
|
||||
// reversed whenever exactly one endpoint sits in a mirrored allocation
|
||||
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
|
||||
// With no 16-bit packed ES image left there is no field order to disagree about; the
|
||||
// client word still round-trips exactly, because the canonical shadow is already
|
||||
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
|
||||
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
|
||||
// path); ForceOff keeps the native narrow storage even where the probe fires - the
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||
// the same capture-less draws and arms the best proven one: the dedicated
|
||||
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
// 0 - the default and the only shipped value until the migration lands - is "pull
|
||||
// everything", i.e. exactly today's behaviour. One bit of it also turns OFF
|
||||
// client-side content addressing of CSOs, which is the negative control the CSO
|
||||
// design is measured against. Accepts decimal or 0x-prefixed hex.
|
||||
Uint64 PipePush = 0;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
// counts instead, for triage and for the lane that must survive to read its own
|
||||
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
|
||||
// off.
|
||||
Bool PipeVerifyFatal = true;
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
|
||||
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
|
||||
// green verify run goes red naming it (negative control A). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipeVerifyCorrupt;
|
||||
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
|
||||
// the value) of that field for that verb, an omission indistinguishable from a
|
||||
// forgotten FillPoints.def row, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
// per-draw staging, counted separately in the stats.
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
// the log whenever PipeStats is on, so a device run needs no writable path.
|
||||
String PipeStatsFile;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+77
-13
@@ -159,35 +159,74 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return static_cast<Uint32>(parsedValue);
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
|
||||
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
|
||||
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
|
||||
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
|
||||
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
|
||||
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const String& value = it->second;
|
||||
const char* text = value.c_str();
|
||||
int base = 10;
|
||||
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
text += 2;
|
||||
base = 16;
|
||||
}
|
||||
char* parseEnd = nullptr;
|
||||
errno = 0;
|
||||
const bool negative = value.find('-') != String::npos;
|
||||
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
|
||||
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
|
||||
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
|
||||
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return static_cast<Uint64>(parsedValue);
|
||||
}
|
||||
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||
features.FixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
||||
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
||||
features.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
@@ -195,8 +234,33 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.ViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||
features.PipeVerifyFatal =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#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/Metrics/PipeStats.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
@@ -42,6 +43,11 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
// off, and idempotent.
|
||||
MG_Util::PipeStats::Shutdown();
|
||||
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||
@@ -102,6 +108,10 @@ namespace MobileGL {
|
||||
MGLOG_I("Initializing MobileGL...");
|
||||
MG_ConfigLoader::Init();
|
||||
MGLOG_I("Config loaded");
|
||||
// Immediately after the config load and before anything can count: the MGPipe
|
||||
// boundary counters latch their enable flag here, so every counting site in the
|
||||
// two backends is a load of an already-settled global for the rest of the run.
|
||||
MG_Util::PipeStats::Init();
|
||||
MG_State::Init();
|
||||
MGLOG_D("MG_State initialized");
|
||||
MG_Backend::Init();
|
||||
|
||||
@@ -192,9 +192,23 @@ namespace MobileGL {
|
||||
void (*MemoryBarrierByRegion)(GLbitfield barriers);
|
||||
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
|
||||
GLenum access, GLenum format);
|
||||
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
|
||||
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
|
||||
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
|
||||
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
|
||||
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
|
||||
// 64-bit and float/double widths are derived there from the same answer, which is why
|
||||
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
|
||||
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
|
||||
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
|
||||
// case for at all.
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
|
||||
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
|
||||
// size - all of which are frontend link artifacts on ProgramObject, and
|
||||
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
|
||||
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
|
||||
// one, or a SPIR-V module), in a namespace the application never sees.
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
@@ -301,6 +315,12 @@ namespace MobileGL {
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
SizeT ShaderStorageBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
@@ -358,6 +378,19 @@ namespace MobileGL {
|
||||
Int MaxFragmentShaderStorageBlocks = 8;
|
||||
Int MaxComputeUniformBlocks = 12;
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
|
||||
// axis. These six, with the invocations limit above, are the only indexed limits a
|
||||
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
|
||||
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
|
||||
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
|
||||
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
|
||||
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
|
||||
// answers, like the invocations limit: GL_Getter and the compile environment floor
|
||||
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
|
||||
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
|
||||
// MaxClipDistances' does.
|
||||
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
|
||||
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
@@ -383,6 +416,19 @@ namespace MobileGL {
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
// backend that cannot host a cull distance MUST report it. The failure this prevents
|
||||
// is worse than the clip one, because cull distance discards the whole primitive:
|
||||
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
|
||||
// gl_MaxCullDistances from it, SPIRV-Cross then emits
|
||||
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
|
||||
// without that extension rejects the program in an info log nobody surfaces. These
|
||||
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
|
||||
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
|
||||
// is: they describe the no-backend case (standalone compiles, unit tests).
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||
@@ -476,6 +522,24 @@ namespace MobileGL {
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||
// independently from the EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||
// varying program-wide (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||
// overrides the detection in either direction at backend init.
|
||||
//
|
||||
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||
// sets it gets standard modules and, at worst, the old honest declines.
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -307,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return capabilities.MaxColorTextureSamples;
|
||||
}
|
||||
|
||||
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
|
||||
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
|
||||
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
|
||||
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
|
||||
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
|
||||
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
|
||||
// in the GLES loader) and the walk below would never run past one sample, recording {1}
|
||||
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
|
||||
// reports 4. Once the frontend validates against this list, that would reject every
|
||||
// multisample renderbuffer on such a context.
|
||||
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
|
||||
GLenum imageFormat) {
|
||||
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
|
||||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
|
||||
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
||||
GLuint texture, TextureInternalFormat format) {
|
||||
GLuint framebuffer = 0;
|
||||
@@ -717,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat));
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
} else {
|
||||
@@ -731,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
@@ -749,11 +766,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLVersion = {4, 6, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -777,7 +794,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
|
||||
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -990,9 +1008,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
@@ -1017,6 +1042,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||
// runs on provides.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
|
||||
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
|
||||
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
|
||||
// buffer the application bound. DirectVulkan reaches the same place through its own
|
||||
// descriptor resolution, so the string is symmetric.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
|
||||
// object-label table are MobileGL's own state, not the host driver's - so it is as
|
||||
// available here as it is on DirectVulkan, which has advertised it all along.
|
||||
E_GL_KHR_debug,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
|
||||
// driver's identical parameters.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
@@ -1065,6 +1161,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
|
||||
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
|
||||
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
|
||||
// reports. So the string follows the host capability rather than the version.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
|
||||
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
|
||||
// over the SAME storage, so that writes through either are visible through the other and
|
||||
// the two carry independent per-texture parameters at the same time - which is exactly
|
||||
// what applications use it for (Better Clouds samples one D24S8 through its own name with
|
||||
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
|
||||
// a single shading pass). A copy-based fallback satisfies neither half, and fails
|
||||
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
|
||||
// the only behaviour that cannot be mistaken for success.
|
||||
//
|
||||
// The host extension is necessary and NOT sufficient, which is why this second gate
|
||||
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
|
||||
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
|
||||
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
|
||||
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
|
||||
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
|
||||
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
|
||||
// land on different renderability carriers the host rejects the pair, the error is
|
||||
// swallowed, and the view is left as a storage-less name that samples as zeros.
|
||||
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
|
||||
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
|
||||
//
|
||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||
// above refuses to tell, just with an extra prerequisite met. Set
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
|
||||
extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
// state is accepted regardless, but forwarding it would be a no-op without the extension,
|
||||
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
|
||||
@@ -1118,8 +1255,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.Clear = Clear;
|
||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||
@@ -1204,6 +1339,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
|
||||
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||
@@ -1278,6 +1415,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
|
||||
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
|
||||
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
// (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
|
||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||
@@ -1340,9 +1484,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
|
||||
// extension pairs the loader probed, independently, because they really do come
|
||||
// separately. False arms the shared phase-B demotion
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
|
||||
// never names the built-in in those stages and needs no extension.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
|
||||
// exercised on a healthy driver (the pinned integration lane); =0 restores the
|
||||
// detected answer's declines.
|
||||
m_dynamicParameters.SupportsTessellationPointSize =
|
||||
m_GLESCapabilities.TessellationPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
m_dynamicParameters.SupportsGeometryPointSize =
|
||||
m_GLESCapabilities.GeometryPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = false;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the driver's extensions");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = true;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
|
||||
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
|
||||
// would reject.
|
||||
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
|
||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
||||
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
||||
|
||||
@@ -78,11 +78,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,8 +92,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
Bool InitWindowSurface(NativeWindowType window);
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,9 +102,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// What an indexed draw has to do about primitive restart before it can be issued.
|
||||
//
|
||||
// Desktop GL restarts on an application-chosen index (glPrimitiveRestartIndex under
|
||||
// GL_PRIMITIVE_RESTART); GLES core restarts only on the all-ones value of the index type
|
||||
// (GL_PRIMITIVE_RESTART_FIXED_INDEX), which the render-state push enables for BOTH caps.
|
||||
// That leaves three cases, and the difference between the last two is not cosmetic - one
|
||||
// adds restarts, the other has to take away restarts the driver would otherwise make.
|
||||
enum class RestartSubstitutionKind : Uint8 {
|
||||
// Nothing to do: restart is off, the fixed-index cap is on, or the application's
|
||||
// restart index already IS the type's all-ones value. The overwhelmingly common answer.
|
||||
None,
|
||||
// The application's index is representable in this index type and differs from the
|
||||
// all-ones value: the index DATA has to be rewritten so the driver restarts where the
|
||||
// application asked.
|
||||
RewriteIndices,
|
||||
// The application's index cannot be held by this index type at all. GL 4.6 core 10.3.6
|
||||
// compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX, so no index can match and the draw restarts NOWHERE - but the
|
||||
// render-state push has already enabled the driver's fixed-index restart, so the
|
||||
// all-ones value has to be un-restarted for the duration of the draw.
|
||||
SuppressRestart,
|
||||
};
|
||||
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType);
|
||||
|
||||
// Turns the driver's fixed-index restart off for one draw and back on afterwards, for the
|
||||
// SuppressRestart case above. Separate from the substitution below because the multi-draw
|
||||
// tiers need it on its own: they rewrite the index stream themselves and only ever need the
|
||||
// cap half. Inert for every other kind, and it never touches the render-state shadow - it
|
||||
// puts the driver back exactly where SyncRenderState left it.
|
||||
class ScopedSuppressedPrimitiveRestart {
|
||||
public:
|
||||
explicit ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind);
|
||||
~ScopedSuppressedPrimitiveRestart();
|
||||
ScopedSuppressedPrimitiveRestart(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
ScopedSuppressedPrimitiveRestart& operator=(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
|
||||
private:
|
||||
Bool m_suppressed = false;
|
||||
};
|
||||
|
||||
// Swaps in a scratch element array buffer holding a copy of the index data in which the
|
||||
// application's restart index has been replaced by the value GLES restarts on. Inert
|
||||
// (and free) unless ResolveRestartSubstitution asks for it. The swap lives for the
|
||||
// object's lifetime, so it covers every pass of a viewport-routed draw, and the previous
|
||||
// GL_ELEMENT_ARRAY_BUFFER name is restored on destruction - which matters beyond tidiness,
|
||||
// because the VAO twin memoises that it already synced that binding.
|
||||
//
|
||||
// The copy may be WIDER than the source (see IndexType): when the source already contains
|
||||
// the type's all-ones value as an ordinary vertex index, that value cannot double as the
|
||||
// restart sentinel, and widening is the only way to keep both meanings. Callers must
|
||||
// therefore take the index type from this object, not from their own argument.
|
||||
class ScopedRestartIndexSubstitution {
|
||||
public:
|
||||
// count/indices describe the draw's index range when the CPU knows it. Pass
|
||||
// count == 0 for an indirect draw, whose count lives in GPU memory: the whole bound
|
||||
// element array buffer is rewritten instead, so every element keeps its position and
|
||||
// a GPU-resident firstIndex - an ELEMENT index, so it survives widening too - still
|
||||
// addresses the index it named.
|
||||
ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices);
|
||||
~ScopedRestartIndexSubstitution();
|
||||
ScopedRestartIndexSubstitution(const ScopedRestartIndexSubstitution&) = delete;
|
||||
ScopedRestartIndexSubstitution& operator=(const ScopedRestartIndexSubstitution&) = delete;
|
||||
|
||||
// False only when a substitution was needed and could not be made. The draw must
|
||||
// then be skipped: issuing it would let the driver silently drop every restart and
|
||||
// weld the primitives on either side together, which is worse than drawing nothing.
|
||||
Bool DrawIsValid() const { return m_valid; }
|
||||
// The element-array offset (or client pointer) the draw must use. Identical to what
|
||||
// was passed in unless a substitution was made.
|
||||
const void* Indices() const { return m_indices; }
|
||||
// The index type the draw must be issued with. Identical to the constructor's unless
|
||||
// the copy had to be widened to keep an all-ones vertex index distinguishable from the
|
||||
// restart sentinel.
|
||||
GLenum IndexType() const { return m_indexType; }
|
||||
|
||||
private:
|
||||
// Declared before m_capOverride so it is initialised first (members initialise in
|
||||
// declaration order): the whole decision is made once, and both the cap override and the
|
||||
// constructor body read the same answer.
|
||||
RestartSubstitutionKind m_kind = RestartSubstitutionKind::None;
|
||||
ScopedSuppressedPrimitiveRestart m_capOverride;
|
||||
const void* m_indices = nullptr;
|
||||
GLenum m_indexType = 0;
|
||||
Uint m_previousBinding = 0;
|
||||
Bool m_substituted = false;
|
||||
Bool m_valid = true;
|
||||
};
|
||||
|
||||
// Drops the scratch element array buffer the substitution above stages through. Like
|
||||
// MultiDrawImpl's scratch names it is abandoned rather than deleted: the name belongs to
|
||||
// the dead ES context, and deleting it would target whatever its successor handed out.
|
||||
void OnRestartSubstitutionContextDestroyed();
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
@@ -141,7 +230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
||||
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
||||
//
|
||||
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// Whether emulation runs. Off only under MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// restores the pre-emulation path as a negative control.
|
||||
Bool ViewportArrayEmulationEnabled();
|
||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||
@@ -372,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the owning thread replaying them: guard both fields with pendingMutex.
|
||||
Bool pendingRespecify = false;
|
||||
VecRange1D pendingRanges;
|
||||
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
|
||||
// stage + glCopyBufferSubData at the next sync; see
|
||||
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
|
||||
// the coherent mapping - an in-place host write tears the in-flight frames
|
||||
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
|
||||
struct PendingResidentWrite {
|
||||
SizeT offset = 0;
|
||||
Vector<Uint8> bytes;
|
||||
};
|
||||
Vector<PendingResidentWrite> pendingResidentWrites;
|
||||
std::mutex pendingMutex;
|
||||
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||
@@ -455,12 +554,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// BackendVertexArrayObject::SyncToBackend.
|
||||
extern Uint64 g_bufferBackendIdGeneration;
|
||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
|
||||
// skips the GL call when the (id, range) already at that index matches, like the
|
||||
// array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
|
||||
// (context may reset).
|
||||
// Binds the transform feedback capture points [0, bufferCount) from the frontend
|
||||
// state, and touches nothing else - in particular it never binds a zero the
|
||||
// application did not ask for. See the definition for why that matters on Mali.
|
||||
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
|
||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||
void InvalidateIndexedBufferBindingCache();
|
||||
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
|
||||
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
|
||||
// calls this on each bind/delete.
|
||||
void InvalidateTransformFeedbackBindingShadows();
|
||||
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
||||
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
||||
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
||||
@@ -510,6 +618,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Present()-time upkeep: records the frame's high-water mark for reclamation
|
||||
// and deletes grown-away ring stores once the GPU is done with them.
|
||||
void UboRingOnPresent();
|
||||
|
||||
// --- Texture unpack-PBO ring ----------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging TEXTURE UPLOADS. A
|
||||
// glTexSubImage from client memory hands the driver a pointer it must read
|
||||
// before the call returns, so the copy has to be ordered against whatever GPU
|
||||
// work still reads the destination texture: Mali resolves that by BLOCKING the
|
||||
// calling thread (osup_sync_object_wait) instead of ghosting, and Minecraft
|
||||
// re-uploads animated atlas sprites and the lightmap every tick into textures
|
||||
// the in-flight frame is still sampling. Staging the bytes into a
|
||||
// GPU-visible unpack PBO and passing an OFFSET instead lets the driver queue
|
||||
// the copy in the command stream with no CPU wait at all.
|
||||
//
|
||||
// Same reclamation contract as the UBO ring: no ring bytes are recycled before
|
||||
// the frame that referenced them completed on the GPU, so a staged block stays
|
||||
// intact for as long as the queued transfer can still be reading it. The store
|
||||
// therefore settles at roughly (bytes staged per frame) x (frames in flight),
|
||||
// which is what to watch if this ring ever shows up in an RSS regression: it
|
||||
// grows on demand from 4 MiB and is capped, not unbounded.
|
||||
//
|
||||
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
|
||||
// EXT_buffer_storage / fences are missing, the ES context is not current, or
|
||||
// ring creation already failed under this context. Callers then upload from
|
||||
// the client pointer exactly as before.
|
||||
Bool UnpackRingAvailable();
|
||||
// Bump-allocate `size` bytes aligned to 64 (a PBO-sourced glTexSubImage only
|
||||
// owes the driver the pixel type's own alignment). Grows the ring when the
|
||||
// in-flight span would be overrun; false when the request exceeds the ring's
|
||||
// size cap or storage (re)creation fails.
|
||||
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset);
|
||||
void* UnpackRingMappedPtr();
|
||||
Uint UnpackRingBufferId();
|
||||
// Largest single staging request the ring can ever satisfy.
|
||||
SizeT UnpackRingMaxBytes();
|
||||
void UnpackRingOnPresent();
|
||||
|
||||
// --- Buffer upload ring ---------------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
|
||||
// (glBufferSubData / non-persistent map flushes) whose destination store may
|
||||
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
|
||||
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
|
||||
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
|
||||
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
|
||||
// each such call serialized against the whole GPU queue (~1 fps while chunks
|
||||
// stream in, and again on every camera pan). App SubData ranges are queued on
|
||||
// the resource instead (the frontend shadow already holds the bytes) and
|
||||
// draw-time sync drains them: bytes staged into this ring, then one
|
||||
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
|
||||
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
|
||||
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
|
||||
// historical immediate-upload path (negative control / escape hatch).
|
||||
void UploadRingOnPresent();
|
||||
} // namespace BufferImpl
|
||||
|
||||
namespace VertexArrayImpl {
|
||||
@@ -792,6 +951,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// The storage half of the sync for a texture created by glTextureView. Instead of
|
||||
// allocating storage and replaying uploads, it makes this object's ES name BE a view
|
||||
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
|
||||
// two names one image and independent per-texture parameters at the same time. The
|
||||
// parameter and sampler halves are unchanged and run on this name as on any other.
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||
@@ -824,6 +995,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// `contextId`/`samplingGeneration` are the frontend context's current
|
||||
// values, hoisted by the caller so a per-draw list walk reads them once
|
||||
// instead of per texture. `t` must be the live frontend texture.
|
||||
// True while a driver-side re-mint has left the parameter caches describing a texture
|
||||
// that no longer exists; SyncTextureObjectToBackend re-pushes them in the same sync.
|
||||
Bool NeedsParameterResync() const { return m_forceTextureParamsResync || m_forceSamplerResync; }
|
||||
|
||||
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||
Uint64 samplingGeneration) const {
|
||||
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||
@@ -867,6 +1042,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
||||
// the split the shader was rewritten against. 0 when this texture takes no split.
|
||||
Uint m_bufferImageSplitViewId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
Uint m_viewSourceBackendTextureId = 0;
|
||||
// For a texture created by glTextureView: the ES name of the storage texture this
|
||||
// one was last made a view OF. EXT_texture_view may be called only once per name, so
|
||||
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
|
||||
// detected here and answered with a fresh name for the view as well - otherwise the
|
||||
// view would keep aliasing storage that no longer exists.
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
@@ -900,7 +1086,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint16 m_syncedShapeParamsVersion = 0;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
UintVec2 m_cacheLodRange = {0, 1000};
|
||||
// All three representations plus the form, because none of them alone identifies the
|
||||
// border colour the driver texture is holding: two integer borders can share one float
|
||||
// (anything differing above 2^24), and a Float -> Int transition can leave every number
|
||||
// unchanged while still needing a different driver entry point.
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 m_cacheBorderColorI = {0, 0, 0, 0};
|
||||
UintVec4 m_cacheBorderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm m_cacheBorderColorForm = BorderColorForm::Float;
|
||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||
@@ -915,6 +1108,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||
// once, or an unchanged version would skip the re-push forever.
|
||||
Bool m_forceTextureParamsResync = false;
|
||||
// The same problem for the FILTER state, which lives in m_cacheSamplerParameters and
|
||||
// is gated on the frontend sampler's version rather than on the params version. A
|
||||
// re-mint leaves that cache describing values the new driver texture never received,
|
||||
// and an unchanged sampler version would then skip re-pushing them forever. This
|
||||
// matters more than mis-filtering: ES makes a texture INCOMPLETE when its filters do
|
||||
// not suit its level set (any integer texture with a non-NEAREST filter, or a
|
||||
// single-level texture with a mipmapping filter), and an incomplete texture samples
|
||||
// (0, 0, 0, 1) rather than its contents.
|
||||
Bool m_forceSamplerResync = false;
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
@@ -1377,6 +1579,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL the same synthesized stage was built with, for
|
||||
// the same reason: ES has neither the state nor an entry point to forward it to, so
|
||||
// glPatchParameterfv's values are compiled in as literals and a program built with one
|
||||
// set is stale for another. Meaningless (and never read) when the patch-vertices field
|
||||
// above is -1, which is the gate the draw path tests first.
|
||||
const FloatVec4& GetPassthroughTessControlOuterLevel() const {
|
||||
return m_passthroughTessControlOuterLevel;
|
||||
}
|
||||
const FloatVec2& GetPassthroughTessControlInnerLevel() const {
|
||||
return m_passthroughTessControlInnerLevel;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1447,6 +1660,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
|
||||
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||
String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames,
|
||||
@@ -1477,6 +1691,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
// The default tessellation levels baked into that same stage. Only meaningful while
|
||||
// the field above is not -1.
|
||||
FloatVec4 m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -29,21 +31,26 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The all-ones value of an index type, which is what GL restarts on once
|
||||
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||
// restart always restarts here and nowhere else.
|
||||
// The index value this batch restarts on, compared at 32 bits against the zero-extended
|
||||
// source index. Normally the all-ones value of the source type, which is what
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
|
||||
// GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
|
||||
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
|
||||
// driver restarts on.
|
||||
//
|
||||
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
|
||||
// restart index the source type cannot hold simply matches nothing, so returning it
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
default: return 0xFFFFFFFFu;
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MGB_CTX->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -78,7 +85,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
const auto& indirect =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
@@ -86,7 +93,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
@@ -151,7 +158,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
// statsClass: which MGPipe byte population these bytes belong to. Counted here
|
||||
// rather than at the four call sites so a new tier cannot forget it.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -164,6 +174,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -178,7 +191,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -202,6 +216,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
@@ -275,10 +292,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer) {
|
||||
Bool hasIndexBuffer, Bool arbitraryRestart) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
|
||||
// only ever restarts on the all-ones value. Every tier but the rebased one hands
|
||||
// the application's own index data to the driver, which would then see no restarts
|
||||
// at all and weld the primitives together. The rebased tier is the one that
|
||||
// REWRITES the stream, and RestartSentinelFor already tells it which value to
|
||||
// translate, so it is the only tier this batch can take.
|
||||
if (arbitraryRestart) {
|
||||
return GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
@@ -402,7 +429,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -488,6 +516,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
|
||||
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
|
||||
// a real vertex index while restarting on a different one is the one shape 32 bits
|
||||
// cannot express - the same corner the single-draw substitution reports.
|
||||
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
|
||||
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
|
||||
"has no wider sentinel to move to.",
|
||||
restartSentinel);
|
||||
}
|
||||
g_indexStaging.resize(total);
|
||||
SizeT cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -507,7 +545,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -712,10 +751,16 @@ void main() {
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
|
||||
return;
|
||||
}
|
||||
// data == nullptr: pure respecify, the compute pass writes the contents, so no
|
||||
// host bytes cross here and nothing is counted.
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
@@ -852,8 +897,14 @@ void main() {
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (drawcount <= 0 || !count || !indices) return;
|
||||
// State-independent and possibly throwing, so it runs before any GL work.
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
// Read before any GL work, because it decides the tier below: a desktop restart index
|
||||
// the driver does not know about can only be honoured by the tier that rewrites the
|
||||
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
|
||||
// needs no rewrite at all - nothing can match it - but it does need the driver's own
|
||||
// fixed-index restart held off for the batch, which is what the scope below does.
|
||||
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
|
||||
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
|
||||
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
|
||||
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
@@ -889,7 +940,8 @@ void main() {
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||
const GLESMultiDrawMode tier =
|
||||
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -921,8 +973,10 @@ void main() {
|
||||
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) {
|
||||
// entry points can receive - except that the base-vertex replay hands the
|
||||
// application's own indices to the driver, which cannot restart on a desktop
|
||||
// restart index, so that batch has only the rewriting floor.
|
||||
if (!drawn && !arbitraryRestart) {
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
#include "Managers.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -26,6 +29,7 @@
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -124,6 +128,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
}
|
||||
// Outside the caveat branch on purpose: the driver CAN create the native narrow
|
||||
// storage - the capability probes say so - it just cannot be trusted as a raw-copy
|
||||
// endpoint. Texture and renderbuffer targets both come through here, which is what
|
||||
// keeps a renderbuffer -> texture copy of these formats same-ES-format when the
|
||||
// widening engages.
|
||||
if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) {
|
||||
options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm;
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
} // namespace
|
||||
@@ -181,6 +193,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return options;
|
||||
}
|
||||
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) {
|
||||
switch (internalFormat) {
|
||||
// TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG
|
||||
// converter folds the two spellings onto one logical format.
|
||||
case TextureInternalFormat::RGB5:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (MG_Config::Features.EsprytWidenPacked16Storage) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
return true;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
return false;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
// Behind the backend gate on purpose: the memoized probe latches its first answer
|
||||
// for the whole process, and before the backend is up the GL function table may
|
||||
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
|
||||
// forever. Once the backend exists, the first narrow-format image this process
|
||||
// creates runs the probe on a live context.
|
||||
if (pActiveBackendObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
|
||||
}
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -712,6 +754,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation) {
|
||||
using Tier = MG_External::GLESCapabilities::PointSizeTier;
|
||||
switch (tier) {
|
||||
case Tier::ExtensionEXT:
|
||||
return tessellation ? "GL_EXT_tessellation_point_size" : "GL_EXT_geometry_point_size";
|
||||
case Tier::ExtensionOES:
|
||||
return tessellation ? "GL_OES_tessellation_point_size" : "GL_OES_geometry_point_size";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The gl_ViewportIndex story, one built-in over: ESSL 320 makes the tessellation and
|
||||
// geometry STAGES core but leaves gl_PointSize out of their gl_PerVertex entirely,
|
||||
// and SPIRV-Cross - which only ever sees a SPIR-V BuiltIn PointSize decoration -
|
||||
// prints the identifier with no directive behind it. Same hard rule as the two
|
||||
// neighbours: never emitted speculatively, because `#extension` on a name the driver
|
||||
// does not advertise is a compile error of its own.
|
||||
if (extensionName == nullptr || glslCode.find(extensionName) != String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
const String directive = String("#extension ") + extensionName + " : require\n";
|
||||
// Right after the #version line, the one position that must stay first;
|
||||
// ForceSupporterOutput's scan for the LAST #extension directive still finds
|
||||
// whichever one that ends up being.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return directive + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + directive;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, directive);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BakeImageFormatQualifiers(String glslCode,
|
||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -836,7 +919,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -866,12 +951,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
for (Uint i = 0; i < 4; ++i) {
|
||||
source += " gl_TessLevelOuter[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
|
||||
}
|
||||
for (Uint i = 0; i < 2; ++i) {
|
||||
source += " gl_TessLevelInner[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
|
||||
}
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
@@ -2208,11 +2295,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
|
||||
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||
|
||||
// Whether this format's ES storage is widened to 8-bit-per-channel because the
|
||||
// driver stores some packed16 allocations with a mirrored field order
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
|
||||
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
|
||||
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
|
||||
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
|
||||
// is no longer 16-bit packed.
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
@@ -273,6 +282,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// error, so this is never emitted speculatively. A no-op when not needed or already
|
||||
// present.
|
||||
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
||||
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
|
||||
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
|
||||
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
|
||||
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
|
||||
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
|
||||
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
|
||||
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
|
||||
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
|
||||
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
|
||||
// advertises neither spelling, because requesting an unadvertised extension is itself a
|
||||
// compile error. A no-op when nullptr or already present.
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName);
|
||||
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
|
||||
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
|
||||
// ones; the two extensions are separate and neither implies the other.
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
@@ -368,11 +393,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
|
||||
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
|
||||
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
|
||||
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
|
||||
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
@@ -382,7 +407,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
@@ -505,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -385,8 +386,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
@@ -500,11 +501,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
@@ -512,9 +513,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
@@ -533,6 +541,79 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
|
||||
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
|
||||
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
|
||||
// every immutable-storage texture (see the comment there).
|
||||
E_GL_ARB_texture_view,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
|
||||
// now reaches the shader on BOTH backends - Magma resolves the lowered
|
||||
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
|
||||
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
|
||||
// landed, because the counter silently read whatever was bound as SSBO N instead.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it. Kept identical to the DirectGLES block so the two
|
||||
// backends do not disagree about what MobileGL is.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
@@ -544,7 +625,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||
@@ -585,6 +666,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
|
||||
// view type cannot be created without the imageCubeArray device feature - so the string
|
||||
// follows the feature, not the version, exactly as the per-layer attachment bit does.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -648,8 +741,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
@@ -693,8 +784,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
@@ -715,7 +806,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
|
||||
m_vulkanCaps.SupportsImageCubeArray);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -763,6 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
@@ -845,6 +938,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
|
||||
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
|
||||
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
|
||||
// them. Not clamped: unlike the block counts these are not amounts an application
|
||||
// allocates, and the frontend already raises them to the GL minimum.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
@@ -912,6 +1014,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it the limit describes a capacity no shader may use, so report none.
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||
// The cull pair, gated on its own feature. shaderCullDistance is separate from
|
||||
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
|
||||
// gate rather than riding on the clip one.
|
||||
m_dynamicParameters.MaxCullDistances =
|
||||
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
|
||||
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
|
||||
// with only one of the two features must not report a combined capacity that implies the
|
||||
// other, so the gate is "either feature" and the value never drops below what is enabled.
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
@@ -974,6 +1089,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||
// detected answer's declines.
|
||||
{
|
||||
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
supportsStagePointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the device feature");
|
||||
supportsStagePointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||
}
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
|
||||
@@ -70,12 +70,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
#include "DirectVulkanResourceState.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/ErrorState/ErrorInfo.h"
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||
#include <atomic>
|
||||
@@ -77,7 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 blockBindingVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||
};
|
||||
|
||||
struct DrawElementsIndirectCommand {
|
||||
@@ -208,16 +209,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
for (auto& module : modules) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < module.entry_point_count; ++entryIndex) {
|
||||
const auto& entryPoint = module.entry_points[entryIndex];
|
||||
if ((entryPoint.shader_stage & SPV_REFLECT_SHADER_STAGE_COMPUTE_BIT) == 0) {
|
||||
continue;
|
||||
}
|
||||
cache.computeWorkGroupSize[0] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.x, 1));
|
||||
cache.computeWorkGroupSize[1] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.y, 1));
|
||||
cache.computeWorkGroupSize[2] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.z, 1));
|
||||
}
|
||||
|
||||
uint32_t bindingCount = 0;
|
||||
SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr);
|
||||
if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) {
|
||||
@@ -277,15 +268,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
if (!MGB_CTX->ValidateProgramName(program)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
auto& programObject = MGB_CTX->GetProgramObject(program);
|
||||
return programObject.get();
|
||||
}
|
||||
|
||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
@@ -344,64 +335,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfi called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfi called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfi called with null GL context");
|
||||
pVulkanRenderer->ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferfv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferuiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferuiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferuiv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferuiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferiv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLfloat* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLuint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
|
||||
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
@@ -409,7 +400,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||
return;
|
||||
@@ -452,13 +443,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
|
||||
|
||||
if (maxdrawcount <= 0) {
|
||||
return;
|
||||
@@ -472,7 +463,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
@@ -503,7 +494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
|
||||
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
@@ -530,7 +521,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsIndirect called with null GL context");
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
@@ -540,7 +531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, 1, 0);
|
||||
return;
|
||||
@@ -574,7 +565,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysInstancedBaseInstance called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
|
||||
|
||||
DrawCmd payload{};
|
||||
payload.mode = mode;
|
||||
@@ -589,11 +580,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysIndirect called with null GL context");
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, 1, 0);
|
||||
return;
|
||||
@@ -623,13 +614,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexImage2D called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexImage2D called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexImage2D called with null GL context");
|
||||
pVulkanRenderer->CopyTexSubImage2D(target, level, 0, 0, x, y, width, height);
|
||||
}
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexSubImage2D called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
@@ -638,32 +629,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
void GenerateMipmap(GLenum target) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GenerateMipmap called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GenerateMipmap called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GenerateMipmap called with null GL context");
|
||||
pVulkanRenderer->GenerateMipmap(target);
|
||||
}
|
||||
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchCompute called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchCompute called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchCompute called with null GL context");
|
||||
pVulkanRenderer->DispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
void DispatchComputeIndirect(GLintptr indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchComputeIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchComputeIndirect called with null GL context");
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
void MemoryBarrier(GLbitfield barriers) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MemoryBarrier called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MemoryBarrier called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MemoryBarrier called with null GL context");
|
||||
pVulkanRenderer->MemoryBarrier(barriers);
|
||||
}
|
||||
|
||||
@@ -682,130 +673,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(void)format;
|
||||
}
|
||||
|
||||
// The two compute limits are the only indexed pnames a backend genuinely owns: they come
|
||||
// from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it
|
||||
// can raise the answer to the GL required minimum. The same six numbers are carried in
|
||||
// DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from
|
||||
// the same limits), which is their MGPCaps carrier once this entry retires - the
|
||||
// AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND
|
||||
// state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit
|
||||
// bindings, the viewport rectangles, the indexed capabilities) and is answered there before
|
||||
// the table is consulted, so the arms this function used to carry for
|
||||
// GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were unreachable duplicates - and not
|
||||
// even faithful ones: the frontend reports the range glBindBufferRange was ASKED for,
|
||||
// verbatim, while these clamped it to the buffer's current storage.
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
|
||||
if (!data) return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer");
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
switch (target) {
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
*data = static_cast<GLint>(
|
||||
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]);
|
||||
return;
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
*data = static_cast<GLint>(
|
||||
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]);
|
||||
return;
|
||||
case GL_SHADER_STORAGE_BUFFER_BINDING: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_START: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
*data = static_cast<GLint>(point.GetRange().start);
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto& range = point.GetRange();
|
||||
const auto start = std::min(range.start, obj->GetSize());
|
||||
const auto end = std::min(range.end, obj->GetSize());
|
||||
*data = static_cast<GLint>(end - start);
|
||||
return;
|
||||
}
|
||||
case GL_IMAGE_BINDING_NAME:
|
||||
case GL_IMAGE_BINDING_LEVEL:
|
||||
case GL_IMAGE_BINDING_LAYERED:
|
||||
case GL_IMAGE_BINDING_LAYER:
|
||||
case GL_IMAGE_BINDING_ACCESS:
|
||||
case GL_IMAGE_BINDING_FORMAT: {
|
||||
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
||||
if (target == GL_IMAGE_BINDING_NAME) {
|
||||
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
|
||||
} else if (target == GL_IMAGE_BINDING_LEVEL) {
|
||||
*data = imageBinding.Level;
|
||||
} else if (target == GL_IMAGE_BINDING_LAYERED) {
|
||||
*data = imageBinding.Layered;
|
||||
} else if (target == GL_IMAGE_BINDING_LAYER) {
|
||||
*data = imageBinding.Layer;
|
||||
} else if (target == GL_IMAGE_BINDING_ACCESS) {
|
||||
*data = static_cast<GLint>(imageBinding.Access);
|
||||
} else {
|
||||
*data = static_cast<GLint>(imageBinding.Format);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
||||
if (!data) return;
|
||||
switch (target) {
|
||||
case GL_SHADER_STORAGE_BUFFER_START: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
*data = static_cast<GLint64>(point.GetRange().start);
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto& range = point.GetRange();
|
||||
const auto start = std::min(range.start, obj->GetSize());
|
||||
const auto end = std::min(range.end, obj->GetSize());
|
||||
*data = static_cast<GLint64>(end - start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
|
||||
if (!params) return;
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) {
|
||||
params[0] = 0;
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: {
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
params[0] = cache.computeWorkGroupSize[0];
|
||||
params[1] = cache.computeWorkGroupSize[1];
|
||||
params[2] = cache.computeWorkGroupSize[2];
|
||||
return;
|
||||
}
|
||||
default:
|
||||
params[0] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || storageBlockName == nullptr) return;
|
||||
@@ -813,7 +714,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||
: 0;
|
||||
if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
MGB_CTX->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
||||
return;
|
||||
@@ -838,24 +739,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ReadPixels called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ReadPixels called with null GL context");
|
||||
pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTexImage called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTexImage called with null GL context");
|
||||
pVulkanRenderer->GetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTextureImage called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTextureImage called with null GL context");
|
||||
pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels);
|
||||
}
|
||||
|
||||
void Clear(GLbitfield mask) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::Clear called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::Clear called with null GL context");
|
||||
pVulkanRenderer->Clear(mask);
|
||||
}
|
||||
|
||||
@@ -883,7 +784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Uint8* indexBytes = nullptr;
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (indexBufferShared != nullptr) {
|
||||
const SizeT offset = reinterpret_cast<SizeT>(indices);
|
||||
@@ -914,7 +815,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArrays called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
if (count < 2) {
|
||||
@@ -939,7 +840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElements called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
@@ -962,7 +863,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArrays called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArrays called with null GL context");
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1007,7 +908,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||
// declined the whole batch and painted nothing.)
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) {
|
||||
@@ -1068,13 +969,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
|
||||
}
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
@@ -1098,14 +999,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitFramebuffer called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::BlitFramebuffer called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::BlitFramebuffer called with null GL context");
|
||||
pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
|
||||
@@ -1206,6 +1107,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
|
||||
// draws, where the renderer's reroute is armed (the affected driver's
|
||||
// stream query counts nothing without an open capture; see
|
||||
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
|
||||
// slots above, which keep the span's XFB-active draws.
|
||||
Vector<Uint32> rerouteSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1215,11 +1122,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale queries are always safe to delete.
|
||||
Uint64 rendererGeneration = 0;
|
||||
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
||||
// capture saw, so a draw made while the span was paused is invisible to it -
|
||||
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
||||
// emitted regardless. The delta closes that gap at result time.
|
||||
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
|
||||
// counts only what the capture saw, so a draw made while the span was paused is
|
||||
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
|
||||
// processing stage emitted regardless. The delta closes that gap at result time.
|
||||
Uint64 pausedPrimitiveSnapshot = 0;
|
||||
// ...unless the GPU already counted those paused draws when the span opened -
|
||||
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
|
||||
// draw with no open capture, paused ones included) or, where the probe measured
|
||||
// the stream query as counting capture-less draws, through the stream slot the
|
||||
// paused draw still takes. Adding the CPU delta on top would count them twice,
|
||||
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
|
||||
// points write it and it answers 0 for GL_PATCHES.
|
||||
Bool pausedPrimitivesCountedByGpu = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1313,13 +1228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
||||
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
|
||||
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
|
||||
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
query->pausedPrimitiveSnapshot;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
@@ -1366,7 +1282,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
query->pausedPrimitiveSnapshot =
|
||||
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||
MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
|
||||
// disarms: the answer is then what this span will actually do for every draw.
|
||||
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -1377,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
|
||||
query->rerouteSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
@@ -1411,5 +1331,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Present() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||
pVulkanRenderer->Present();
|
||||
// THE frame boundary for the MGPipe counters, at the backend entry point rather
|
||||
// than inside VulkanRenderer::Present: that function has an early return for the
|
||||
// no-usable-swapchain case, and a suspended frame is still a frame the counters
|
||||
// must close.
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::OnPresent();
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -95,8 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
|
||||
@@ -201,11 +201,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
@@ -435,6 +440,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -37,11 +37,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
|
||||
// identity and both are hashed. The renderer leaves the enable false unless the
|
||||
// device's sampleRateShading feature was enabled
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
|
||||
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
|
||||
// the levels glPatchParameterfv set are compiled INTO that module and are not a
|
||||
// function of the program or of patchControlPoints - see the note on
|
||||
// passthroughTessControlStage.
|
||||
Uint64 passthroughTessControlKey = 0;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
@@ -87,8 +115,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
|
||||
// of the default tessellation levels - the first two of which ComputeHash already
|
||||
// mixes in, and the third of which arrives through passthroughTessControlKey above.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
// in the order glslang declares them - which is the order a redeclaration must use.
|
||||
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
|
||||
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
|
||||
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
|
||||
enum class PerVertexMemberBit : Uint32 {
|
||||
Position = 1u << 0,
|
||||
PointSize = 1u << 1,
|
||||
ClipDistance = 1u << 2,
|
||||
CullDistance = 1u << 3,
|
||||
};
|
||||
// What a program parsed below #version 450 carries, and the fallback when a module's
|
||||
// block cannot be read.
|
||||
static constexpr Uint32 kDefaultPerVertexMembers =
|
||||
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
|
||||
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
@@ -185,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
// The program has a tessellation or geometry module declaring TessellationPointSize /
|
||||
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
|
||||
// is off, so a pipeline built from it is invalid usage
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
|
||||
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
|
||||
// implements one level up, and the counterpart of the DirectGLES arm that reports a
|
||||
// driver with neither point-size extension by name.
|
||||
//
|
||||
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
@@ -194,6 +237,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
|
||||
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
|
||||
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
|
||||
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
|
||||
// which the source processor rewrites to "#version 460 core" - carries four members
|
||||
// where a 430 program carries three. A fixed three-member pass-through fed the
|
||||
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
|
||||
// this whole family is written around.
|
||||
Uint32 passthroughPerVertexMembers = 0;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -249,6 +301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -267,6 +320,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -311,6 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -329,6 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -396,12 +452,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool tessellationAndGeometryPointSizeEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
@@ -448,6 +506,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
|
||||
// sampler form above deliberately does not answer. Kept separate rather than folded in
|
||||
// because the two are asked in different places for different reasons: a sampler's domain
|
||||
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
|
||||
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
|
||||
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
|
||||
// format at all, and the numeric domain is then the only thing that constrains it.
|
||||
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
@@ -477,18 +543,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
|
||||
// member set, because all three decide what the generator emits. The size comes from
|
||||
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
|
||||
// - draw state rather than link state, and the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
|
||||
// own SPIR-V, so two programs at different GLSL versions need different modules. The
|
||||
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
|
||||
// part of that pipeline's identity. Compiling is bounded by the number of distinct
|
||||
// (size, levels, members) combinations a program draws with - one or two in practice -
|
||||
// and only ever happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The identity of one such module: everything the generator bakes in, folded into a
|
||||
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
|
||||
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
|
||||
// same value into the pipeline hash so a pipeline can never be handed a module built for
|
||||
// different levels or a different block shape.
|
||||
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
|
||||
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
|
||||
// Input variable that is an array of a Block-decorated struct). Zero when the module has
|
||||
// no such block. Exposed for tests, which is the only way to pin the shape agreement
|
||||
// without a device.
|
||||
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -531,6 +619,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
|
||||
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
|
||||
// GeometryPointSize is refused at build time (see VkProgramObject::
|
||||
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
@@ -548,11 +641,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// Pass-through tessellation control stages by the identity of what was compiled into
|
||||
// them - the input patch size and the six default tessellation levels, folded into one
|
||||
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
|
||||
// cannot simply be keyed on the patch size any more). A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
//
|
||||
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
|
||||
// nothing, so an application that recomputes a level per frame mints a new key per frame.
|
||||
// Reaching the cap destroys every module and starts over (see the flush in
|
||||
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
|
||||
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
|
||||
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
|
||||
// texel buffer, and there is no other route to that answer from here.
|
||||
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||
@@ -176,7 +179,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
|
||||
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
|
||||
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
|
||||
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
|
||||
// one shared object, because a descriptor whose format disagrees with the shader's
|
||||
// declaration is invalid Vulkan even when nothing ever reads it.
|
||||
//
|
||||
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
|
||||
// texel buffer, which never carries one, or for a formatless `writeonly` image);
|
||||
// `numericDomain` decides the format when there is no declaration and is the fallback
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
@@ -251,6 +290,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet);
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VkBufferManager* m_bufferManager = nullptr;
|
||||
ProgramFactory* m_programFactory = nullptr;
|
||||
Vector<FrameResources> m_frames;
|
||||
@@ -263,6 +303,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale memo.
|
||||
//
|
||||
// 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
|
||||
// memos outlive this factory (they live on the frontend context'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
|
||||
|
||||
@@ -10,12 +10,23 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
constexpr SizeT kLiveResourcePruneThreshold = 256;
|
||||
|
||||
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
|
||||
// every minStorageBufferOffsetAlignment in the wild, and free.
|
||||
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
|
||||
// See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
|
||||
// same reason plus one: a texel buffer view's range must be a whole number of texels of
|
||||
// whatever format the placeholder is asked for, and 256 divides by every texel size in
|
||||
// the GL image-format table (1, 2, 4, 8 and 16 bytes).
|
||||
constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;
|
||||
|
||||
// A zero-copy persistent buffer is created once and never recreated (the app holds
|
||||
// its mapped pointer), and may be bound to any role, so it carries every usage.
|
||||
// TRANSFER_DST is added by CreateResidentStorage.
|
||||
@@ -130,6 +141,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
m_transientUploadArena.Shutdown();
|
||||
m_unboundStorageBuffer.Destroy();
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
DestroyAllDeferredReleases();
|
||||
ReleaseAllLiveResources();
|
||||
m_copyProvider = nullptr;
|
||||
@@ -218,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
|
||||
(void)kind;
|
||||
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
|
||||
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
|
||||
// absent: its bytes are counted by the caller, which is the only place that
|
||||
// knows whether the payload is the default block (stage-ubo-global) or a named
|
||||
// one repacked into the ring (stage-ubo-named), and counting here as well would
|
||||
// double every uniform byte.
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Index:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Indirect:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::TextureBuffer:
|
||||
case BufferKind::ShaderStorage:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Uniform:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::InitializeTransientArenas() {
|
||||
@@ -328,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource.pendingFullUpload = false;
|
||||
return true;
|
||||
}
|
||||
@@ -342,6 +388,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), 16, staging)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
|
||||
// half of the same bytes and is not counted twice.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commandBuffer == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
@@ -411,6 +462,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +489,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -473,6 +529,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -543,6 +602,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint8* seed = bufferObject.MappedData();
|
||||
if (seed != nullptr) {
|
||||
resource->buffer.Upload(seed, size, 0);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The one-time seed of a persistent map. Everything the app writes AFTER
|
||||
// this goes straight through the mapping and is persistent-map-push
|
||||
// territory (unwired, D4/D-B4), not this class.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
resource->persistentMapped = true;
|
||||
resource->pendingFullUpload = false;
|
||||
@@ -591,6 +657,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->usageFlags = 0;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
resource->pendingFullUpload = false;
|
||||
}
|
||||
|
||||
@@ -670,6 +740,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource->transientSlice = outSlice;
|
||||
resource->transientFrameSerial = m_frameSerial;
|
||||
resource->transientChangeSerial = changeSerial;
|
||||
@@ -706,6 +779,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredResourceReleases[frameIndex].clear();
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
|
||||
if (!m_unboundStorageBuffer.IsValid()) {
|
||||
if (m_initInfo.allocator == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// Host-visible so the zero fill needs no command buffer: this can be reached from
|
||||
// descriptor resolution, which runs inside an already-open recording and must not
|
||||
// start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
|
||||
// block rather than 4 bytes so that a shader which does read the block gets a
|
||||
// plausible unsized-array length instead of one that rounds to zero.
|
||||
const Bool created = m_unboundStorageBuffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = kUnboundStorageDescriptorBytes,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
});
|
||||
if (!created) {
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
|
||||
m_unboundStorageBuffer.Destroy();
|
||||
return {};
|
||||
}
|
||||
if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
|
||||
Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
|
||||
}
|
||||
}
|
||||
return m_unboundStorageBuffer.GetSlice();
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
|
||||
if (!m_unboundTexelBuffer.IsValid()) {
|
||||
if (m_initInfo.allocator == nullptr) {
|
||||
return {};
|
||||
}
|
||||
// A SECOND placeholder rather than more usage bits on the storage-block one. The two
|
||||
// are independent failure domains: a device that refuses this allocation must not
|
||||
// take the storage-block placeholder - and with it the fix this one is a sibling of -
|
||||
// down with it. Host-visible and zero-filled for the same reason as that one: this is
|
||||
// reached from descriptor resolution, inside an already-open recording, which must
|
||||
// not start a copy of its own.
|
||||
const Bool created = m_unboundTexelBuffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = kUnboundTexelBufferDescriptorBytes,
|
||||
.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
});
|
||||
if (!created) {
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
return {};
|
||||
}
|
||||
if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
|
||||
Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
|
||||
}
|
||||
}
|
||||
return m_unboundTexelBuffer.GetSlice();
|
||||
}
|
||||
|
||||
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
|
||||
@@ -120,6 +120,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
|
||||
VkDeviceSize alignment, BufferSlice& outSlice);
|
||||
|
||||
// The descriptor a shader storage block gets when the program declares it and the
|
||||
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
|
||||
// legal state - the block simply has no store, so reads are undefined and writes go
|
||||
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
|
||||
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
|
||||
// buffer, created once and shared by every unbound binding: bindings that are only
|
||||
// declared (the case this exists for) never touch it, and one that is actually read
|
||||
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
|
||||
// that indexes past it.
|
||||
BufferSlice AcquireUnboundStorageDescriptor();
|
||||
|
||||
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
|
||||
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
|
||||
// it has no GL buffer attached. Both are legal GL states that make a fetch return
|
||||
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
|
||||
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
|
||||
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
|
||||
// and the descriptor is a VkBufferView, not a buffer - is built by
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
@@ -180,6 +201,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkBufferManagerInitInfo m_initInfo{};
|
||||
BufferArena m_transientUploadArena;
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
// with the manager.
|
||||
VkBufferObject m_unboundStorageBuffer;
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
|
||||
#include "VkClearManager.h"
|
||||
|
||||
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
|
||||
// clear key's layer span has to be the same one the render pass builds its attachment view from.
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
@@ -50,7 +55,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (payload.colorEncoding != ClearColorEncoding::Float) return;
|
||||
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
|
||||
// is exactly right and there is nothing to undo.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
|
||||
|
||||
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
|
||||
@@ -100,13 +105,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentLayerCount(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
|
||||
// the entry - so a layered cube map's glClear reached one face and the other five were lost
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
@@ -122,8 +127,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -157,6 +184,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -286,9 +322,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(texture.get());
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -305,8 +343,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -321,6 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -411,6 +456,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
@@ -67,28 +68,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
|
||||
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
|
||||
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
|
||||
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
|
||||
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
|
||||
// they resolve the layer through their own copies of this helper, which do shift.
|
||||
const auto* texture = attachment.GetTexture().get();
|
||||
if (attachment.IsLayered()) {
|
||||
return 0;
|
||||
return ToStorageArrayLayer(texture, 0);
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
|
||||
}
|
||||
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
const Int face =
|
||||
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
|
||||
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
|
||||
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
|
||||
|
||||
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
|
||||
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
|
||||
// type is not a legal answer for several of the targets GL can attach, and returning it
|
||||
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
|
||||
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
|
||||
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
|
||||
// or dereferenced - by the driver inside vkCreateFramebuffer.
|
||||
//
|
||||
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
|
||||
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
|
||||
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
|
||||
//
|
||||
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
|
||||
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
|
||||
// the only honest answer would be to decline the attachment - and every driver this has run on,
|
||||
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
|
||||
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
|
||||
static VkImageViewType ResolveAttachmentViewType(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const VkTextureManager::TextureResource& resource) {
|
||||
if (attachment.IsLayered()) {
|
||||
return resource.viewType;
|
||||
switch (resource.viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
default:
|
||||
return resource.viewType;
|
||||
}
|
||||
}
|
||||
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||
@@ -96,8 +127,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||
// symmetry with CUBE_ARRAY.
|
||||
//
|
||||
// 3D belongs in the same list and was missing from it, which is why the "per-slice
|
||||
// attachment view is a 2D view whose array layer is the slice" branch in
|
||||
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
|
||||
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
|
||||
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
|
||||
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return resource.viewType;
|
||||
@@ -318,47 +356,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
|
||||
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
|
||||
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
|
||||
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
|
||||
//
|
||||
// This used to be a hand-maintained second copy of that table, and it was missing exactly
|
||||
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
|
||||
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
|
||||
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
|
||||
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
|
||||
// a size-incompatible vkCmdCopyImage.
|
||||
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -602,7 +611,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
@@ -633,11 +642,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
|
||||
att.GetTextureLevel()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
|
||||
att.GetTextureLayer()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
@@ -754,7 +765,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
@@ -840,7 +851,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,13 +875,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
@@ -952,7 +963,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
@@ -993,8 +1004,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
|
||||
"%u on FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||
continue;
|
||||
@@ -1006,7 +1021,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
|
||||
auto& att = fbo.GetAttachment(drawbuf);
|
||||
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
|
||||
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
|
||||
const auto textureTarget = texture->GetTarget();
|
||||
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||
attachmentDescriptions.emplace_back();
|
||||
@@ -1049,8 +1064,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
const IntVec2 attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent);
|
||||
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
|
||||
// 1D-array attachment's GL height is its layer count, and using it as the
|
||||
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
|
||||
// image it is built over.
|
||||
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
|
||||
if (width == 0)
|
||||
width = attachmentExtent.x();
|
||||
if (height == 0)
|
||||
@@ -1078,12 +1097,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
if (textureResource == nullptr) {
|
||||
// SyncTextureResource legitimately declines - an unsupported format,
|
||||
// sample count or image-flag combination, or a vkCreateImage the driver
|
||||
// refused. There is no image to attach, so there is no render pass.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
|
||||
"attachment %u on FBO %u; declining the render pass",
|
||||
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
@@ -1100,8 +1126,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
|
||||
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
|
||||
"render pass",
|
||||
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
desc.samples = attachmentSampleCount;
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
@@ -1144,7 +1177,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (a.IsTexture() && b.IsTexture()) {
|
||||
return a.GetTexture().get() == b.GetTexture().get() &&
|
||||
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
|
||||
a.GetTextureLevel() == b.GetTextureLevel();
|
||||
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
|
||||
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
|
||||
}
|
||||
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
|
||||
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
|
||||
@@ -1193,20 +1227,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
|
||||
MOBILEGL_ASSERT(depthTextureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
|
||||
if (depthTextureResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthTextureResource->layout;
|
||||
depthAttachmentDescription.format = depthTextureResource->format;
|
||||
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
||||
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
|
||||
attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
|
||||
swapchainExtent);
|
||||
attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo,
|
||||
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
|
||||
swapchainExtent);
|
||||
} else {
|
||||
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
||||
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
MOBILEGL_ASSERT(depthRenderbufferResource,
|
||||
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
|
||||
if (depthRenderbufferResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthRenderbufferResource->layout;
|
||||
depthAttachmentDescription.format = depthRenderbufferResource->format;
|
||||
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
|
||||
@@ -1254,7 +1297,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
const Uint32 attachmentMipLevel =
|
||||
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
|
||||
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
|
||||
selectedDepthStencilAttachment->GetTextureLevel());
|
||||
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
||||
@@ -1279,8 +1323,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
|
||||
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1302,6 +1352,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(depthRenderbufferResource->view);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1399,8 +1455,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
|
||||
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
|
||||
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
|
||||
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
|
||||
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
|
||||
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
|
||||
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
|
||||
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
|
||||
// line on every draw for the life of the process - a decline caches nothing, so every
|
||||
// later draw to the same framebuffer re-enters this path and fails again.
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
const VkResult renderPassResult =
|
||||
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
|
||||
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
|
||||
"render pass",
|
||||
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
VkFramebufferCreateInfo framebufferCreateInfo;
|
||||
@@ -1413,8 +1486,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
framebufferCreateInfo.width = width;
|
||||
framebufferCreateInfo.height = height;
|
||||
framebufferCreateInfo.layers = framebufferLayers;
|
||||
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer));
|
||||
const VkResult framebufferResult =
|
||||
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
|
||||
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
|
||||
// The render pass has no entry to own it yet, so it is destroyed here rather than
|
||||
// leaked - RenderPassEntry's destructor is the only other thing that would.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
|
||||
"%u attachments, %u layers); declining the render pass",
|
||||
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
|
||||
fbo.GetExternalIndex(), width, height,
|
||||
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
|
||||
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
IntVec2 extent = {width, height};
|
||||
RenderPassEntry renderPassEntry {
|
||||
hash,
|
||||
@@ -1439,7 +1525,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
return insertedIt->second;
|
||||
return &insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
|
||||
@@ -243,9 +243,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
|
||||
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
}
|
||||
|
||||
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
|
||||
// family and an integer family and requires the sampler's choice to match the image view's
|
||||
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
|
||||
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
|
||||
enum class BorderColorDomain {
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger
|
||||
};
|
||||
|
||||
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return BorderColorDomain::SignedInteger;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return BorderColorDomain::UnsignedInteger;
|
||||
default:
|
||||
return BorderColorDomain::Float;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
|
||||
// format in which texture components are stored. For signed and unsigned normalized
|
||||
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
|
||||
// floating-point and integer formats, border values are clamped to the representable range of
|
||||
// the format." Every clause of that sentence is a real case here - the clamp is not just the
|
||||
// normalized one.
|
||||
//
|
||||
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
|
||||
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
|
||||
// so a negative border on them must come back as 0 rather than as a negative number the
|
||||
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
|
||||
struct FloatBorderRange {
|
||||
Bool clamped = true;
|
||||
Float minValue = 0.0f;
|
||||
Float maxValue = 1.0f;
|
||||
};
|
||||
|
||||
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
return {false, 0.0f, 0.0f};
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
return {true, -65504.0f, 65504.0f};
|
||||
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
|
||||
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
|
||||
// that matters for correctness is the lower one, and a single conservative upper bound
|
||||
// costs nothing a real border colour will ever notice.
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
return {true, 0.0f, 64512.0f};
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
return {true, 0.0f, 65408.0f};
|
||||
default:
|
||||
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Per-component representable range of an integer texture format, as Int64 so that the whole
|
||||
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
|
||||
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
|
||||
// whose alpha is narrower than its colour channels.
|
||||
struct IntegerBorderRange {
|
||||
Int64 rgbMin = 0;
|
||||
Int64 rgbMax = 0;
|
||||
Int64 alphaMin = 0;
|
||||
Int64 alphaMax = 0;
|
||||
};
|
||||
|
||||
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
|
||||
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
return uniform(-128, 127);
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
return uniform(-32768, 32767);
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
return uniform(0, 255);
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
return uniform(0, 65535);
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
return uniform(0, 4294967295LL);
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return {0, 1023, 0, 3};
|
||||
default:
|
||||
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
|
||||
// clamp that cannot alter a value the GL entry points could have carried.
|
||||
return uniform(-2147483648LL, 2147483647LL);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsDepthTextureFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
m_customBorderColorSupported = initInfo.customBorderColorSupported;
|
||||
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
m_customBorderColorSupported = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.borderColor = borderColor.color;
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
|
||||
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
|
||||
// customBorderColorWithoutFormat was required alongside customBorderColors at device
|
||||
// creation - a GL sampler object has no idea which texture it will be paired with.
|
||||
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
|
||||
if (borderColor.isCustom) {
|
||||
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
|
||||
customBorderColorInfo.customBorderColor = borderColor.customValue;
|
||||
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
|
||||
customBorderColorInfo.pNext = samplerInfo.pNext;
|
||||
samplerInfo.pNext = &customBorderColorInfo;
|
||||
}
|
||||
|
||||
VkSampler vkSampler = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
|
||||
|
||||
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.usesCustomBorderColor = borderColor.isCustom;
|
||||
if (entry.usesCustomBorderColor) {
|
||||
++m_customBorderColorSamplerCount;
|
||||
}
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
ResolvedBorderColor resolved{};
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
}
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
const auto& borderColor = sampler.GetBorderColor();
|
||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||
const auto format = texture.GetFormat();
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
|
||||
if (isDepthTexture) {
|
||||
if (domain != BorderColorDomain::Float) {
|
||||
// An integer image view REQUIRES an integer border colour, whatever the value is - even
|
||||
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
|
||||
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
|
||||
// representation (a plain cast) is as good an answer as any.
|
||||
//
|
||||
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
|
||||
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
|
||||
//
|
||||
// Which representation to start from is the TEXTURE's domain, not the entry-point form
|
||||
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
|
||||
// an integer internal data type and does not define a sign conversion between the two
|
||||
// integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
|
||||
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
|
||||
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
|
||||
// the representation by the FORM instead would answer 0 here, which is a defensible
|
||||
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
|
||||
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
|
||||
// agreement decides it.
|
||||
const auto range = ResolveIntegerBorderRange(format);
|
||||
const auto& borderColorI = sampler.GetBorderColorI();
|
||||
const auto& borderColorUI = sampler.GetBorderColorUI();
|
||||
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
|
||||
Int64 clamped[4];
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
|
||||
: static_cast<Int64>(borderColorI[channel]);
|
||||
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
|
||||
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
|
||||
clamped[channel] = std::clamp(raw, low, high);
|
||||
}
|
||||
|
||||
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
|
||||
// the palette entry it clamps to rather than missing every one of them.
|
||||
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
|
||||
if (allZeroRgb && clamped[3] == 0) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (allZeroRgb && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (domain == BorderColorDomain::UnsignedInteger) {
|
||||
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
|
||||
} else {
|
||||
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
// No custom colour available: pick the nearest of the three integer palette entries
|
||||
// rather than always answering transparent black, which is what turned an integer border
|
||||
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
|
||||
const Bool opaque = clamped[3] != 0;
|
||||
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
|
||||
// format, so it is clamped to that format's representable range first. Without the clamp the
|
||||
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
|
||||
// and fell through to transparent black - every border texel sampled 0 where the test wanted
|
||||
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
|
||||
// 32-bit float formats are unclamped.
|
||||
FloatVec4 borderColor = sampler.GetBorderColor();
|
||||
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
|
||||
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.y(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.z(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.w(), range.minValue, range.maxValue));
|
||||
}
|
||||
|
||||
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
|
||||
if (IsDepthTextureFormat(format)) {
|
||||
if (NearlyEqual(borderColor.x(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
|
||||
NearlyEqual(borderColor.z(), 0.0f);
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
|
||||
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
resolved.customValue.float32[0] = borderColor.x();
|
||||
resolved.customValue.float32[1] = borderColor.y();
|
||||
resolved.customValue.float32[2] = borderColor.z();
|
||||
resolved.customValue.float32[3] = borderColor.w();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Nearest of the three float palette entries. Transparent black stays the answer for a
|
||||
// transparent border, which is what the old unconditional fallback got right by accident.
|
||||
const Bool opaque = borderColor.w() >= 0.5f;
|
||||
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -28,6 +28,13 @@ public:
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
|
||||
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
|
||||
Bool customBorderColorSupported = false;
|
||||
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
|
||||
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
|
||||
// and falls back to the snapped predefined value once it is reached.
|
||||
Uint32 maxCustomBorderColorSamplers = 0;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
@@ -52,6 +59,21 @@ public:
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
|
||||
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
|
||||
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
|
||||
//
|
||||
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
|
||||
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
|
||||
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
|
||||
// custom colour was affordable is decided before the key is built, not twice with a budget
|
||||
// change in between.
|
||||
struct ResolvedBorderColor {
|
||||
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
VkClearColorValue customValue{};
|
||||
Bool isCustom = false;
|
||||
};
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
@@ -60,17 +82,18 @@ private:
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
|
||||
Bool usesCustomBorderColor = false;
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
|
||||
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
@@ -82,6 +105,12 @@ private:
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
Bool m_customBorderColorSupported = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
|
||||
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
|
||||
// and Shutdown.
|
||||
Uint32 m_customBorderColorSamplerCount = 0;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
@@ -46,13 +48,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
struct TextureShapeInfo {
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
@@ -220,6 +215,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
|
||||
MG_State::GLState::ITextureObject* texture) {
|
||||
// A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it
|
||||
// views, not by itself. Everything this identity keys - the TextureResource, the tracked
|
||||
// image layout, the alive-object weak reference, the storage-usage marks, the per-draw
|
||||
// sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing
|
||||
// the resolution here rather than at each call site is what makes it impossible to miss
|
||||
// one: a layout update posted against a view's own identity would have found no resource
|
||||
// at all, which is precisely how an attached view came back blank.
|
||||
//
|
||||
// One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the
|
||||
// root at creation, so a storage owner is never itself a view.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity{
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -364,7 +375,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
@@ -694,6 +705,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||
m_viewRequestedImageFlags.erase(identity);
|
||||
m_viewRequestedFormats.erase(identity);
|
||||
auto resourceIt = m_textureResources.find(identity);
|
||||
if (resourceIt != m_textureResources.end()) {
|
||||
DeferResourceRelease(Move(resourceIt->second));
|
||||
@@ -737,9 +750,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_drawSyncedThisDraw.clear();
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
||||
|
||||
// A GL texture view has no image of its own; it resolves to - and shares - the resource
|
||||
// of the texture whose storage it views, so that there is exactly one VkImage, one
|
||||
// tracked layout and one upload path per storage. Everything that makes the view a
|
||||
// different texture (format, level/layer window, sampled aspect) is applied where the
|
||||
// VkImageViews are built, keyed in alternateSampledViews / attachmentViews.
|
||||
MG_State::GLState::ITextureObject& texture = StorageTextureOf(textureOrView);
|
||||
if (&texture != &textureOrView) {
|
||||
NoteTextureViewImageRequirements(textureOrView, texture);
|
||||
}
|
||||
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
|
||||
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
|
||||
@@ -784,7 +807,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
const auto& liveTexture = MGB_CTX->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
aliveTexture = liveTexture;
|
||||
} else {
|
||||
@@ -838,7 +861,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A GL texture view shares this resource with the texture it views, so it must not touch
|
||||
// perMipViews: that vector is indexed by mip level alone and holds views built with the
|
||||
// STORAGE texture's format and full layer range. Route it through the keyed attachment
|
||||
// cache instead, where its own window is part of the key.
|
||||
if (texture.IsTextureView()) {
|
||||
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
|
||||
window.viewType);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -866,21 +901,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 layerCount,
|
||||
VkImageViewType viewType) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
|
||||
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
|
||||
// SyncTextureResource asks for and may have had refused per format.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
|
||||
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
|
||||
// view still contributes here is its own internal format, which may reinterpret the
|
||||
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
|
||||
// through.
|
||||
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
|
||||
if (texture.IsTextureView()) {
|
||||
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
|
||||
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
|
||||
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
|
||||
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
|
||||
// means). BOTH spellings are legal only on a 2D-array-compatible image
|
||||
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
|
||||
// may have had refused per format.
|
||||
//
|
||||
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
|
||||
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
|
||||
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
|
||||
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
|
||||
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
|
||||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
|
||||
"2D-array-compatible=%d)",
|
||||
// Not an error line: the render-pass builder turns the null view into one
|
||||
// MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
|
||||
"slices, 2D-array-compatible=%d)",
|
||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||
mipLevel, sliceCount,
|
||||
static_cast<Int>(viewType), mipLevel, sliceCount,
|
||||
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -893,11 +950,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat baseAttachmentFormat =
|
||||
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
|
||||
const VkFormat attachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
|
||||
|
||||
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType) {
|
||||
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
|
||||
// for a view every field below is part of what distinguishes it from its parent.
|
||||
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
|
||||
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -932,7 +994,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
|
||||
// own format and aspect, so a GL view has to go to the keyed cache.
|
||||
if (texture.IsTextureView()) {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
|
||||
window.baseMipLevel = mipLevel;
|
||||
window.levelCount = 1;
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -960,14 +1037,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
|
||||
resource->sampledView == VK_NULL_HANDLE) {
|
||||
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
|
||||
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
|
||||
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
|
||||
// in the same cache while both name the same image, the same levels and the same layers.
|
||||
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource,
|
||||
const TextureViewWindow& window) {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = window.baseMipLevel,
|
||||
.levelCount = window.levelCount,
|
||||
.baseArrayLayer = window.baseArrayLayer,
|
||||
.layerCount = window.layerCount,
|
||||
.viewType = window.viewType,
|
||||
.format = window.format,
|
||||
.aspect = window.sampledAspect,
|
||||
.componentSwizzle = PackComponentSwizzle(window.components),
|
||||
};
|
||||
const auto existing = resource.alternateSampledViews.find(key);
|
||||
if (existing != resource.alternateSampledViews.end()) {
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
if (window.format != resource.format &&
|
||||
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
|
||||
"(image format=%d)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Int>(resource.format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const VkImageView view =
|
||||
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
|
||||
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
|
||||
&window.components);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
|
||||
"mips=[%u,%u) layers=[%u,%u)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
|
||||
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
|
||||
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
|
||||
window.baseArrayLayer + window.layerCount);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
resource.alternateSampledViews.emplace(key, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// A GL texture view never has a sampledView of its own on this resource - that one
|
||||
// belongs to the storage texture, with the storage texture's format, level range and
|
||||
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
|
||||
// keyed cache, even when the requested format happens to match the image's.
|
||||
if (texture.IsTextureView()) {
|
||||
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
|
||||
if (format != VK_FORMAT_UNDEFINED) {
|
||||
window.format = format;
|
||||
}
|
||||
return GetOrCreateWindowedSampledView(texture, *resource, window);
|
||||
}
|
||||
|
||||
if (resource->sampledView == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
|
||||
return resource->sampledView;
|
||||
}
|
||||
@@ -987,8 +1126,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = resource->sampledBaseMipLevel,
|
||||
.levelCount = resource->sampledLevelCount,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = resource->arrayLayers,
|
||||
.viewType = resource->viewType,
|
||||
.format = format,
|
||||
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.componentSwizzle = PackComponentSwizzle(
|
||||
ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()))),
|
||||
};
|
||||
const auto existing = resource->alternateSampledViews.find(key);
|
||||
if (existing != resource->alternateSampledViews.end()) {
|
||||
@@ -1029,6 +1173,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel, VkFormat format,
|
||||
Bool layered, Int32 layer) {
|
||||
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
|
||||
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
@@ -1053,8 +1199,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = resource->arrayLayers;
|
||||
// A GL texture view opens onto a WINDOW of the storage's layers; a layered image
|
||||
// binding of it must not reach past that window into the parent's other layers.
|
||||
Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0);
|
||||
Uint32 layerCount = texture.IsTextureView()
|
||||
? std::min(static_cast<Uint32>(texture.GetViewNumLayers()),
|
||||
resource->arrayLayers - baseArrayLayer)
|
||||
: resource->arrayLayers;
|
||||
VkImageViewType viewType = resource->viewType;
|
||||
if (!layered) {
|
||||
switch (resource->viewType) {
|
||||
@@ -1087,7 +1238,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType;
|
||||
if (format == resource->format && isFullResourceView) {
|
||||
if (format == resource->format && isFullResourceView && !texture.IsTextureView()) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -1613,7 +1764,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool storageUpgradePending =
|
||||
!outResource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||
// Same shape for a GL texture view's demands on the image (MUTABLE_FORMAT for a
|
||||
// format-reinterpreting view, CUBE_COMPATIBLE for a cube view of an array texture):
|
||||
// nothing about the texture itself changed, but the live image cannot carry the view.
|
||||
// Masked by what this format can actually be given: MUTABLE_FORMAT is deliberately
|
||||
// withheld from formats the driver already refused it for (see SyncTextureResource), and
|
||||
// without this mask the "upgrade still pending" test below could never come true again -
|
||||
// costing every later sync of that texture the whole slow path, forever.
|
||||
VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (m_mutableFormatUnsupported.find(outResource.format) != m_mutableFormatUnsupported.end()) {
|
||||
requestedViewFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
const Bool viewFlagUpgradePending =
|
||||
(outResource.imageCreateFlags & requestedViewFlags) != requestedViewFlags;
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && !viewFlagUpgradePending &&
|
||||
outResource.syncedContentVersion == syncingContentVersion &&
|
||||
outResource.syncedShapeVersion == syncingShapeVersion &&
|
||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||
@@ -1807,6 +1971,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
// Flags a GL texture view over this storage asked for (see NoteTextureViewImageRequirements).
|
||||
// MUTABLE_FORMAT is still withheld from formats the driver has already refused it for, so a
|
||||
// reinterpreting view degrades to no view rather than to no texture.
|
||||
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
|
||||
if (requestedViewFlags != 0) {
|
||||
imageCreateFlags |= requestedViewFlags;
|
||||
if (m_mutableFormatUnsupported.find(format) != m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
}
|
||||
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
|
||||
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
|
||||
// views - multisample sRGB render targets included.
|
||||
@@ -1967,6 +2141,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
// ...plus every format a glTextureView over this storage reinterprets it as. Those
|
||||
// are NOT enumerable from ResolveSampledImageViewFormat - an application may name any
|
||||
// member of the format's view class (GL 4.6 core table 8.21) - so without this the
|
||||
// list would forbid the very view the MUTABLE_FORMAT bit was requested for.
|
||||
AppendViewRequestedFormats(texture, viewFormats);
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
@@ -1999,12 +2178,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
// Losing 2D-array compatibility only costs framebuffer attachment of this format's
|
||||
// 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
|
||||
// the z axis; failing creation would lose the texture entirely. Recorded here (the
|
||||
// per-format set below) so later syncs neither reprobe nor flag-mismatch against this
|
||||
// image and recreate it, and so GetOrCreateAttachmentViewAtMipLevel declines rather
|
||||
// than handing back a view that cannot exist - the render-pass builder then turns
|
||||
// that decline into a skipped draw instead of a null VkImageView in pAttachments.
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
"textureId=%d; creating without it (per-slice and layered framebuffer "
|
||||
"attachment of 3D textures in this format will be unavailable)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
m_2dArrayCompatibleUnsupported.insert(format);
|
||||
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
@@ -2392,6 +2575,164 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
|
||||
}
|
||||
|
||||
MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf(
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
const auto& storageOwner = texture.GetViewStorageOwner();
|
||||
return storageOwner ? *storageOwner : texture;
|
||||
}
|
||||
|
||||
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
|
||||
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
|
||||
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
|
||||
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
|
||||
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
|
||||
VkImageViewType storageViewType) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return VK_IMAGE_VIEW_TYPE_1D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
default:
|
||||
return storageViewType;
|
||||
}
|
||||
}
|
||||
|
||||
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
|
||||
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
|
||||
TextureViewWindow window{};
|
||||
window.format = resource.format;
|
||||
window.viewType = resource.viewType;
|
||||
window.baseArrayLayer = 0;
|
||||
window.layerCount = resource.arrayLayers;
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||
window.components = ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()));
|
||||
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
|
||||
if (!texture.IsTextureView()) {
|
||||
return window;
|
||||
}
|
||||
|
||||
window.isTextureView = true;
|
||||
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
|
||||
// view, so ResolveViewMipRange above already clamped them against the view's own level
|
||||
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
|
||||
// into the storage image's numbering.
|
||||
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
|
||||
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
|
||||
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
|
||||
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
|
||||
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED) {
|
||||
window.format = viewFormat;
|
||||
}
|
||||
// Recomputed against the view's own format: a depth/stencil storage viewed as
|
||||
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
|
||||
// one parameter Better Clouds deliberately sets differently on the two names.
|
||||
window.sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
|
||||
? GetAspectMaskForFormat(window.format)
|
||||
: resource.aspect,
|
||||
texture.GetDepthStencilTextureMode());
|
||||
|
||||
// Clamp to what the image actually has; a malformed view must degrade to an empty range
|
||||
// rather than reach vkCreateImageView with an out-of-bounds subresource.
|
||||
if (window.baseMipLevel >= resource.mipLevels) {
|
||||
window.baseMipLevel = resource.mipLevels - 1;
|
||||
window.levelCount = 1;
|
||||
} else {
|
||||
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
|
||||
}
|
||||
if (window.levelCount == 0) window.levelCount = 1;
|
||||
if (window.baseArrayLayer >= resource.arrayLayers) {
|
||||
window.baseArrayLayer = resource.arrayLayers - 1;
|
||||
window.layerCount = 1;
|
||||
} else {
|
||||
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
|
||||
}
|
||||
if (window.layerCount == 0) window.layerCount = 1;
|
||||
return window;
|
||||
}
|
||||
|
||||
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
|
||||
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
|
||||
// resolve of a view already creates - or recreates and copies forward - an image the view can
|
||||
// legally be built over, instead of handing back VK_NULL_HANDLE for a frame.
|
||||
void VkTextureManager::NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture) {
|
||||
const TextureIdentity storageIdentity = MakeTextureIdentity(&storageTexture);
|
||||
VkImageCreateFlags required = 0;
|
||||
const VkFormat viewFormat = ResolveTextureFormatInfo(viewTexture.GetFormat()).format;
|
||||
const VkFormat storageFormat = ResolveTextureFormatInfo(storageTexture.GetFormat()).format;
|
||||
if (viewFormat != VK_FORMAT_UNDEFINED && storageFormat != VK_FORMAT_UNDEFINED &&
|
||||
viewFormat != storageFormat) {
|
||||
required |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
// The image may be created with a NARROWED format list (see SyncTextureResource), and
|
||||
// that list is a promise about every format the image will ever be viewed as. Record
|
||||
// this one so the promise stays true.
|
||||
m_viewRequestedFormats[storageIdentity].insert(viewFormat);
|
||||
}
|
||||
const TextureTarget viewTarget = viewTexture.GetTarget();
|
||||
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
|
||||
// Only when the storage could legally carry the bit. VK_IMAGE_CREATE_CUBE_COMPATIBLE
|
||||
// demands a 2D image with square levels and at least six array layers
|
||||
// (VUID-VkImageCreateInfo-flags-00954), and asking for it on a storage that has fewer
|
||||
// would fail vkCreateImage - which, because SyncTextureResource has already released
|
||||
// the old resource by then, would leave the PARENT texture with no image at all. A
|
||||
// degenerate view must not be able to destroy the texture it views; let its own view
|
||||
// creation fail instead.
|
||||
const IntVec3 storageSize = storageTexture.GetBaseSize();
|
||||
const Bool storageCanBeCube = storageSize.x() == storageSize.y() &&
|
||||
storageTexture.GetViewNumLayers() >= 6 &&
|
||||
storageTexture.GetTarget() != TextureTarget::Texture3D;
|
||||
if (storageCanBeCube) {
|
||||
required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
} else {
|
||||
MGLOG_W_ONCE("Texture view %d wants a cube view of texture %d, whose storage is %dx%d with %u "
|
||||
"layers and cannot be cube-compatible; the view will have no image view.",
|
||||
viewTexture.GetExternalIndex(), storageTexture.GetExternalIndex(), storageSize.x(),
|
||||
storageSize.y(), storageTexture.GetViewNumLayers());
|
||||
}
|
||||
}
|
||||
if (required == 0) {
|
||||
return;
|
||||
}
|
||||
VkImageCreateFlags& stored = m_viewRequestedImageFlags[storageIdentity];
|
||||
stored |= required;
|
||||
}
|
||||
|
||||
VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags(
|
||||
const MG_State::GLState::ITextureObject& storageTexture) const {
|
||||
const auto it = m_viewRequestedImageFlags.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
return it == m_viewRequestedImageFlags.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
void VkTextureManager::AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const {
|
||||
const auto it = m_viewRequestedFormats.find(
|
||||
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
|
||||
if (it == m_viewRequestedFormats.end()) {
|
||||
return;
|
||||
}
|
||||
for (const VkFormat viewFormat : it->second) {
|
||||
if (std::find(outFormats.begin(), outFormats.end(), viewFormat) == outFormats.end()) {
|
||||
outFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
|
||||
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
|
||||
|
||||
@@ -2811,6 +3152,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
packBox(dst, item.regionLo, item.regionSize);
|
||||
}
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// Same shape split as Espryt's: one union box per item, or one job per rect of
|
||||
// a refined rect list. The box/rect decision is invisible to SSIM and is what
|
||||
// the +6 ms/frame Mali cliff of section 7.3 was, so it is counted apart from
|
||||
// the bytes.
|
||||
Uint64 boxEmissions = 0;
|
||||
Uint64 rectEmissions = 0;
|
||||
Uint64 jobs = 0;
|
||||
for (const auto& item : uploadItems) {
|
||||
if (item.rects.empty()) {
|
||||
++boxEmissions;
|
||||
jobs += isCombinedDepthStencil ? 2u : 1u;
|
||||
} else {
|
||||
++rectEmissions;
|
||||
jobs += static_cast<Uint64>(item.rects.size());
|
||||
}
|
||||
}
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
|
||||
static_cast<Uint64>(stagingSize));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions,
|
||||
static_cast<Uint64>(uploadItems.size()));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, boxEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadRectEmissions, rectEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, jobs);
|
||||
}
|
||||
|
||||
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags uploadSrcAccessMask = 0;
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -22,6 +24,31 @@ class ITextureObject;
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
|
||||
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
|
||||
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
|
||||
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
|
||||
// because Vulkan devices rarely support the 3-channel layouts.
|
||||
//
|
||||
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
|
||||
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
|
||||
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
|
||||
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
|
||||
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
|
||||
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
|
||||
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
|
||||
// `.format` and ignore the rest.
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
|
||||
|
||||
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
||||
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
||||
@@ -41,6 +68,58 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
|
||||
return glTexelSize;
|
||||
}
|
||||
|
||||
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
|
||||
//
|
||||
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
|
||||
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
|
||||
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
|
||||
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
|
||||
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
|
||||
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
|
||||
// depending only on which path happens to consume it first - and the materialise path then POPS
|
||||
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
|
||||
// that split gets introduced; keep them the same function.
|
||||
//
|
||||
// Two shapes make this more than `size.z()`:
|
||||
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
|
||||
// just above), so z reads 1 and every layer above the first was silently dropped.
|
||||
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
|
||||
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
|
||||
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
|
||||
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
|
||||
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsLayered()) {
|
||||
return 1u;
|
||||
}
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
if (target == TextureTarget::TextureCubeMap) {
|
||||
return 6u;
|
||||
}
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
|
||||
// storage texture's (VkTextureManager::StorageTextureOf).
|
||||
//
|
||||
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
|
||||
// GetOrCreate*View entry point below expects values that have already been through here, and so
|
||||
// does everything that reads or copies an attachment directly. Both are identity on a plain
|
||||
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
|
||||
// and there is no second, view-only code path to keep in step.
|
||||
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
|
||||
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
|
||||
}
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
@@ -139,17 +218,35 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
|
||||
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
|
||||
// through two GL names in one draw, the parent with the stencil aspect and the view with
|
||||
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
|
||||
// same image. Without these two fields those views would alias each other in the cache.
|
||||
struct SampledImageViewKey {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
|
||||
// same window but different swizzles are different views. Baked into the key because
|
||||
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
|
||||
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
|
||||
// version moves, nothing else would ever notice a swizzle change on a view.
|
||||
Uint32 componentSwizzle = 0;
|
||||
|
||||
Bool operator==(const SampledImageViewKey& other) const {
|
||||
return baseMipLevel == other.baseMipLevel &&
|
||||
levelCount == other.levelCount &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format;
|
||||
format == other.format &&
|
||||
aspect == other.aspect &&
|
||||
componentSwizzle == other.componentSwizzle;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,10 +254,15 @@ public:
|
||||
SizeT operator()(const SampledImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -358,6 +460,58 @@ public:
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
|
||||
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
|
||||
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
|
||||
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
|
||||
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
|
||||
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
|
||||
// layout field, so a second resource aliasing the same image would desynchronise the moment
|
||||
// either of them transitioned it.
|
||||
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// The window a GL texture object opens onto its storage image. For a plain texture this is
|
||||
// the resource's own full extent; for a view it is the sub-range, format and aspect
|
||||
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
|
||||
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
|
||||
// the storage texture's own defaults.
|
||||
struct TextureViewWindow {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
|
||||
VK_COMPONENT_SWIZZLE_A};
|
||||
Bool isTextureView = false;
|
||||
};
|
||||
|
||||
// The four component swizzles packed into one value, for the sampled-view cache key.
|
||||
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
|
||||
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
|
||||
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
|
||||
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
|
||||
}
|
||||
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
|
||||
const TextureResource& resource) const;
|
||||
// Records what a GL texture view needs of the image it views, so the next sync of the
|
||||
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
|
||||
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
|
||||
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture);
|
||||
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
|
||||
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with.
|
||||
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const;
|
||||
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
|
||||
// image. Shared back end of every GL-texture-view sampled path.
|
||||
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource, const TextureViewWindow& window);
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
@@ -572,6 +726,19 @@ private:
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
|
||||
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
|
||||
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
|
||||
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
|
||||
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
|
||||
// feature almost none of them use. A SAME-format view - which is the common case, and the
|
||||
// Better Clouds case - needs no flag at all and therefore costs nothing.
|
||||
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
|
||||
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
|
||||
// that NO other format will ever be viewed, and building a view outside it is
|
||||
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
|
||||
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
@@ -563,7 +564,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Native subgroup topology, queried at device creation for the compute-module
|
||||
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
||||
// stage flag; 0 / false when the device has no usable compute subgroups or
|
||||
// MOBILEGL_DISABLE_SUBGROUP forced them off.
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
|
||||
Uint32 m_nativeSubgroupSize = 0;
|
||||
Bool m_nativeSubgroupSupported = false;
|
||||
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||
@@ -584,6 +585,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
|
||||
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
|
||||
// and capturable by name through transform feedback). Cached at device creation and
|
||||
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
|
||||
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
|
||||
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
|
||||
// that is invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
|
||||
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
|
||||
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
|
||||
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
|
||||
// colour outside the palette has to be snapped to the nearest predefined one. Both features
|
||||
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
|
||||
// colour without naming the image format it will be paired with, which GL's sampler objects
|
||||
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
|
||||
// to be able to fall back to the snapped value once it is reached.
|
||||
Bool m_customBorderColorFeatureEnabled = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
|
||||
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
|
||||
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
|
||||
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
|
||||
Bool m_sampleRateShadingFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
@@ -650,8 +675,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// per object: one group of four slots each, handed out on first use.
|
||||
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||
Uint32 m_xfbNextCounterSlot = 0;
|
||||
// Which transform feedback object owns each slot group, by the frontend's never-reused
|
||||
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
|
||||
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
|
||||
// inherited the dead one's slot, and since nothing ever removed an entry the map also
|
||||
// grew for the life of the context. A fixed table cannot do either: a group is taken over
|
||||
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
|
||||
// counters can still be resumed never loses them, and a dead object's group comes back.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
|
||||
// Tie-break among reclaimable groups only; never on its own, because the paused span the
|
||||
// groups exist for is by construction the least recently used one.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
|
||||
Uint64 m_xfbCounterSlotUseSerial = 0;
|
||||
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||
// counter-buffer resume on the next captured draw of the same span.
|
||||
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||
@@ -693,15 +728,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
|
||||
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
|
||||
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
|
||||
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
|
||||
// finds that defect with a working control - or
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
|
||||
// GENERATED count through this pool instead, whose type the arming picks:
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
|
||||
// query with its rasterizer-discard feature (exact semantics by definition -
|
||||
// the extension exists because GL needs this count without a capture), else a
|
||||
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
|
||||
// per primitive reaching primitive clipping - after every vertex processing
|
||||
// stage, before rasterizer discard - which is the same set).
|
||||
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
|
||||
// every draw with no open capture - a PAUSED span's draws included - takes a
|
||||
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
|
||||
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
|
||||
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
|
||||
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
|
||||
// GL query span may therefore hold slots of both pools.
|
||||
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
|
||||
// VK_EXT_primitives_generated_query: base feature, and the
|
||||
// ...WithRasterizerDiscard feature without which a discarding draw inside the
|
||||
// query is invalid usage (so the reroute never picks the dedicated pool on a
|
||||
// base-only device - GL applications toggle discard freely).
|
||||
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||
// tessellationShader was enabled at device creation (it is taken whenever the
|
||||
// device advertises it); gates the probe's PATCHES shape.
|
||||
Bool m_tessellationShaderFeatureEnabled = false;
|
||||
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
|
||||
MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||
// The bring-up probe measured this device's stream query as counting draws made
|
||||
// with no capture span open (the StreamCounts verdict) - so it counts the
|
||||
// PAUSED-span ones too, through the stream slot they take when nothing is
|
||||
// rerouted. Only the probe can know this, so it stays false wherever the probe
|
||||
// is not consulted (the forced arms), which keeps those lanes' accounting as it
|
||||
// was.
|
||||
Bool m_primGenStreamCountsXfbInactiveDraws = false;
|
||||
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
|
||||
Uint32 m_primGenRerouteSlotCursor = 0;
|
||||
Vector<Uint32> m_primGenRerouteActiveSlots;
|
||||
Bool m_primGenRerouteSlotOpen = false;
|
||||
Uint32 m_primGenRerouteOpenSlot = 0;
|
||||
// Runs the bring-up probe (memoized per process) and decides
|
||||
// m_primGenRerouteKind. Called at the end of device creation: it records on
|
||||
// m_graphicsQueue, which nothing else is using yet.
|
||||
void ArmPrimGenReroute();
|
||||
|
||||
public:
|
||||
// Whether a GENERATED span opened now will have the draws made while the GL
|
||||
// span is PAUSED counted on the GPU - through the reroute pool, which takes
|
||||
// every draw with no open capture, or (where the reroute is not armed because
|
||||
// the stream query was measured to count capture-less draws) through the stream
|
||||
// slot such a draw still takes. The frontend's CPU paused-primitive counter
|
||||
// must not be added on top of either: it would double count, and it cannot
|
||||
// price the draws that matter anyway - only 3 of the ~15 draw entry points
|
||||
// write it and it answers 0 for GL_PATCHES. Read once per span, after
|
||||
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
|
||||
Bool ArePausedDrawsGpuCounted() const;
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||
Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
@@ -733,6 +827,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||
// index type, neither of which the mode/program/state hashes carry. Without it an
|
||||
// indexed and a non-indexed draw over the same program and state collide on one entry
|
||||
// and the second one gets the first one's restart setting.
|
||||
Bool primitiveRestartEnable = false;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
@@ -746,9 +846,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const;
|
||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||
// and the pipeline-state memo word so the two cannot disagree.
|
||||
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||
// it (the effective sample mask), so a draw that changes only the target's sample count
|
||||
// has to recompute rather than reuse.
|
||||
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Uint64 m_pipelineStateHash = 0;
|
||||
Bool m_pipelineStateHashValid = false;
|
||||
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||
@@ -790,7 +899,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
|
||||
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
|
||||
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
|
||||
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
|
||||
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
|
||||
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
|
||||
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
|
||||
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
|
||||
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
|
||||
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
|
||||
@@ -801,6 +912,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_lastSampledSetProgramVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
Uint64 m_lastSampledSetSamplingGeneration = 0;
|
||||
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
|
||||
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
|
||||
// VkProgramObject::xfbCaptureDeclined.
|
||||
Bool m_currentDrawXfbCaptureDeclined = false;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
@@ -865,6 +981,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
|
||||
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
|
||||
// already separates indexed from non-indexed draws, but not one index TYPE from
|
||||
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
|
||||
// makes those two draws want different pipelines.
|
||||
Bool primitiveRestartEnable = false;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
@@ -893,6 +1015,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// probe the pipeline memo after a state change without re-fetching the
|
||||
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||
Uint32 renderPassColorCount = 0;
|
||||
// Pinned with the colour count and for the same reason: the fast path recomputes the
|
||||
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
|
||||
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||
@@ -1168,13 +1293,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CreateSwapchain();
|
||||
void CreateCommandPool();
|
||||
|
||||
// Whether THIS draw's primitive stream restarts, and therefore what
|
||||
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
|
||||
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
|
||||
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
|
||||
// and the index TYPE (an application restart index that does not fit the type matches no
|
||||
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
|
||||
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
||||
const IndexBufferView* pIndexBufferView) const;
|
||||
|
||||
VkPipeline GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
@@ -1241,13 +1376,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLenum filter);
|
||||
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
|
||||
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
|
||||
// Clears one layer of a colour image through a throwaway render pass whose entire content
|
||||
// is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
|
||||
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
|
||||
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
|
||||
// layout the caller already tracks for the whole image, so this never has to touch
|
||||
// resource->layout.
|
||||
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue);
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
// The multisample arm of the above. Split out rather than branched inline because it
|
||||
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
|
||||
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
|
||||
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
const Vector<PendingClearEntry>& pendingClears);
|
||||
Bool MaterializePendingClearForRenderbuffer(
|
||||
VkCommandBuffer commandBuffer,
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
|
||||
@@ -39,18 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||
!MG_Config::Features.DisableSubgroup;
|
||||
!MG_Config::Features.MagmaDisableSubgroup;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||
// grows one under-declared array; every other module passes through untouched.
|
||||
return MG_Config::Features.FixIterationRPSubgroupScratch !=
|
||||
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
|
||||
MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPBarrier() {
|
||||
return MG_Config::Features.IterationRPFixBarrier;
|
||||
return MG_Config::Features.MagmaIterationRPFixBarrier;
|
||||
}
|
||||
|
||||
inline Bool ShouldDeriveNumSubgroups() {
|
||||
@@ -58,6 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
||||
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
||||
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.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
|
||||
|
||||
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
|
||||
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
|
||||
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
|
||||
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
const char* MGPipeVerbName(MGPipeVerb verb) {
|
||||
const auto index = static_cast<SizeT>(verb);
|
||||
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
|
||||
}
|
||||
|
||||
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
|
||||
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
|
||||
}
|
||||
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
|
||||
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
namespace {
|
||||
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
|
||||
|
||||
// Every overload is declared up front: the array overloads recurse into their element
|
||||
// type, and a call inside a template only sees what was declared before the template.
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
|
||||
template <class T>
|
||||
void CorruptStorage(T& v);
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p);
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p);
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]);
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v);
|
||||
|
||||
// ---- equality over one field's storage ----
|
||||
// O-class storage compares by identity: a raw pointer into the context, or the object a
|
||||
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
|
||||
// C arrays element-wise.
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b) {
|
||||
return a == b;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
|
||||
return a.get() == b.get();
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
|
||||
for (SizeT i = 0; i < N; ++i) {
|
||||
if (!StorageEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
|
||||
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
|
||||
}
|
||||
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
|
||||
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
|
||||
// what turns a fourth member into a build break rather than a blind spot.
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
|
||||
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
|
||||
"CurrentVertexAttributeValue grew a member; update the comparator");
|
||||
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b) {
|
||||
return MGPipeFieldEqual(a, b);
|
||||
}
|
||||
|
||||
// ---- corruption of one field's storage ----
|
||||
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
|
||||
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
|
||||
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
|
||||
// flipped address with no control block, an array corrupts its first element, and any
|
||||
// other struct has its first byte XOR'ed with 0x5A.
|
||||
template <class T>
|
||||
T* FlipPointer(T* p) {
|
||||
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p) {
|
||||
p = FlipPointer(p);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p) {
|
||||
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]) {
|
||||
CorruptStorage(a[0]);
|
||||
}
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
|
||||
CorruptStorage(c.Blend);
|
||||
}
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v) {
|
||||
v.floatValue[0] += 1.f;
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T& v) {
|
||||
if constexpr (std::is_same_v<T, Bool>) {
|
||||
v = !v;
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
|
||||
} else if constexpr (std::is_arithmetic_v<T>) {
|
||||
v = static_cast<T>(v + 1);
|
||||
} else {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
|
||||
unsigned char first = 0;
|
||||
std::memcpy(&first, &v, 1);
|
||||
first ^= 0x5A;
|
||||
std::memcpy(&v, &first, 1);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
|
||||
// A forwarded field has no storage and is equal by definition; VisitStorage answers
|
||||
// false for it, hence the explicit sticky test first.
|
||||
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
|
||||
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
|
||||
}
|
||||
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField) {
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
|
||||
if (outField != nullptr) *outField = field;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
|
||||
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
|
||||
CorruptStorage(x);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,714 @@
|
||||
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
// The frontend types the accessors return. Allowed here: P13 keeps this include for the
|
||||
// verify arm (ARCHITECTURE.md 9.5). This header spells no MG_State global - every read of
|
||||
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
|
||||
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
|
||||
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
|
||||
// there without dragging MG_Remote in.
|
||||
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
|
||||
MOBILEGL_PIPE_VERIFY)
|
||||
#define MOBILEGL_PIPE_POISON 1
|
||||
#else
|
||||
#define MOBILEGL_PIPE_POISON 0
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
|
||||
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
|
||||
// MOBILEGL_ASSERT, which is inert in INFO builds.
|
||||
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
|
||||
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
|
||||
const char* MGPipeVerbName(MGPipeVerb verb);
|
||||
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
|
||||
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
|
||||
|
||||
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
|
||||
// a read of a field whose stamp is older than the current verb serial is
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
#define MGP_INPUT_CHECK(Field) \
|
||||
do { \
|
||||
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
|
||||
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
|
||||
} \
|
||||
} while (0)
|
||||
#else
|
||||
#define MGP_INPUT_CHECK(Field) ((void)0)
|
||||
#endif
|
||||
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
|
||||
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
|
||||
// against the stored value, and reports the FIRST divergence as
|
||||
// Fatal{PipeVerifyDiffer, "Field@Verb", verb=<serial>, where=read} (the indices go in a
|
||||
// preceding MGLOG_E). Only the live block (gPipeInputs) is verified; a snapshot's own
|
||||
// accessors are plain loads. Off in every other build.
|
||||
struct PipeInputs;
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1);
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) \
|
||||
::MobileGL::MG_Pipe::MGPipeVerifyReadHook(*this, (Field), static_cast<Uint>(Index0), static_cast<Uint>(Index1))
|
||||
#else
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
|
||||
#endif
|
||||
|
||||
// The V/O storage of every field that has storage, by field id. The seven F-class
|
||||
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
|
||||
// is what keeps the comparator and the corruption injector one function each instead of
|
||||
// two sixty-way switches.
|
||||
// clang-format off
|
||||
#define MGP_INPUT_STORAGE_LIST(X) \
|
||||
X(GetActiveTextureUnit, m_activeTextureUnit) \
|
||||
X(GetBlendColor, m_blendColor) \
|
||||
X(GetBlendEquationIndexed, m_blendEquation) \
|
||||
X(GetBlendFuncIndexed, m_blendFunc) \
|
||||
X(GetBoundTransformFeedbackName, m_boundTransformFeedbackName) \
|
||||
X(GetBoundVertexArray, m_boundVertexArray) \
|
||||
X(GetBufferBindingSlot, m_bufferBindingSlot) \
|
||||
X(GetBufferBindingPoint, m_bufferBindingPointBase) \
|
||||
X(GetTouchedBufferBindingPointCount, m_touchedBindingPointCount) \
|
||||
X(GetClampReadColor, m_clampReadColor) \
|
||||
X(GetClearColor, m_clearColor) \
|
||||
X(GetClearDepth, m_clearDepth) \
|
||||
X(GetClearStencil, m_clearStencil) \
|
||||
X(GetColorMaskIndexed, m_colorMask) \
|
||||
X(GetCullFaceMode, m_cullFaceMode) \
|
||||
X(GetCurrentVertexAttribute, m_currentVertexAttribute) \
|
||||
X(GetDepthFunc, m_depthFunc) \
|
||||
X(GetDepthMask, m_depthMask) \
|
||||
X(GetDepthRangeIndexed, m_depthRange) \
|
||||
X(GetFramebufferBindingSlot, m_framebufferBindingSlot) \
|
||||
X(GetImageTextureBinding, m_imageTextureBindingBase) \
|
||||
X(GetLineWidth, m_lineWidth) \
|
||||
X(GetLogicOp, m_logicOp) \
|
||||
X(GetMaxTouchedTextureUnit, m_maxTouchedTextureUnit) \
|
||||
X(GetMinSampleShadingValue, m_minSampleShadingValue) \
|
||||
X(GetPatchDefaultInnerLevel, m_patchDefaultInnerLevel) \
|
||||
X(GetPatchDefaultOuterLevel, m_patchDefaultOuterLevel) \
|
||||
X(GetPatchVertices, m_patchVertices) \
|
||||
X(GetPipelineStateVersion, m_pipelineStateVersion) \
|
||||
X(GetPixelStoreParameters, m_pixelStore) \
|
||||
X(GetPolygonModeFront, m_polygonModeFront) \
|
||||
X(GetPolygonOffsetFactor, m_polygonOffsetFactor) \
|
||||
X(GetPolygonOffsetUnits, m_polygonOffsetUnits) \
|
||||
X(GetPrimitiveRestartIndex, m_primitiveRestartIndex) \
|
||||
X(GetProgramForDispatch, m_programForDispatch) \
|
||||
X(GetProgramForDraw, m_programForDraw) \
|
||||
X(GetProvokingVertexMode, m_provokingVertexMode) \
|
||||
X(GetRenderStateParameters, m_renderState) \
|
||||
X(GetRenderStateParametersVersion, m_renderStateParametersVersion) \
|
||||
X(GetSamplingResolutionGeneration, m_samplingResolutionGeneration) \
|
||||
X(GetScissorBox, m_scissorBox) \
|
||||
X(GetStencilState, m_stencil) \
|
||||
X(GetTextureBindGeneration, m_textureBindGeneration) \
|
||||
X(GetTextureContextId, m_textureContextId) \
|
||||
X(GetTextureUnitObject, m_textureUnitBase) \
|
||||
X(GetTransformFeedbackCapturedVertices, m_transformFeedbackCapturedVertices) \
|
||||
X(GetTransformFeedbackGeneration, m_transformFeedbackGeneration) \
|
||||
X(GetTransformFeedbackPausedPrimitiveCounter, m_transformFeedbackPausedPrimitiveCounter) \
|
||||
X(GetTransformFeedbackProgram, m_transformFeedbackProgram) \
|
||||
X(GetViewport, m_viewport) \
|
||||
X(GetViewportIndexed, m_viewportIndexed) \
|
||||
X(IsCapabilityEnabled, m_capability) \
|
||||
X(IsCapabilityEnabledIndexed, m_capabilityIndexed) \
|
||||
X(IsTransformFeedbackActive, m_transformFeedbackActive) \
|
||||
X(IsTransformFeedbackPaused, m_transformFeedbackPaused) \
|
||||
X(GetBoundTransformFeedbackLifetimeId, m_boundTransformFeedbackLifetimeId)
|
||||
// clang-format on
|
||||
|
||||
// The seven F-class fields, for the arithmetic below and for the sticky table's proof.
|
||||
// The forwarded set IS the sticky set (PipeFields.def marks the same seven rows F and
|
||||
// sticky), so an eighth sticky row without a forwarder is refused here, not by a test.
|
||||
inline constexpr SizeT kMGPipeForwardedFieldCount = 7;
|
||||
static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount,
|
||||
"the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows");
|
||||
|
||||
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
|
||||
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
|
||||
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
|
||||
// sed is type-neutral:
|
||||
//
|
||||
// V (value) copied out of GLContext at fill time by calling the same accessor;
|
||||
// no derivation logic is re-implemented here, which is what keeps the
|
||||
// copy semantically identical by construction.
|
||||
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
|
||||
// slot/array for the accessors that return a non-const reference into
|
||||
// the context. Identity is what phase C turns into a handle.
|
||||
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
|
||||
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
|
||||
// context may be spelled). Sticky: stamped once by the first fill that
|
||||
// sees a live context.
|
||||
//
|
||||
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
|
||||
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
|
||||
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
|
||||
struct PipeInputs {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using BufferObject = MG_State::GLState::BufferObject;
|
||||
using BufferTarget = ::MobileGL::BufferTarget;
|
||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||
using FramebufferTarget = ::MobileGL::FramebufferTarget;
|
||||
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
|
||||
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||
using ITextureObject = MG_State::GLState::ITextureObject;
|
||||
using TextureUnit = MG_State::GLState::TextureUnit;
|
||||
using ImageTextureBinding = MG_State::GLState::ImageTextureBinding;
|
||||
using CurrentVertexAttributeValue = MG_State::GLState::CurrentVertexAttributeValue;
|
||||
|
||||
static constexpr SizeT kBufferTargetCount = static_cast<SizeT>(BufferTarget::BufferTargetCount);
|
||||
static constexpr SizeT kFramebufferTargetCount = static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount);
|
||||
static constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
|
||||
static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
|
||||
static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS;
|
||||
static constexpr SizeT kStencilFaceCount = static_cast<SizeT>(StencilFace::StencilFaceCount);
|
||||
|
||||
// IsCapabilityEnabledIndexed's two indexed capabilities, the only ones GLContext keeps
|
||||
// indexed state for (RenderState::IsCapabilityEnabledIndexed).
|
||||
struct IndexedCapabilities {
|
||||
Bool Blend[kMGMaxDrawBuffers];
|
||||
Bool ScissorTest[kMaxViewports];
|
||||
};
|
||||
|
||||
// ---- identity / liveness (not fields) ----
|
||||
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
|
||||
// must be true as soon as a context exists, fill or no fill, which is what today's
|
||||
// null-context guards test.
|
||||
Bool IsLive() const;
|
||||
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
|
||||
const void* ContextIdentity() const { return m_contextIdentity; }
|
||||
// The verb of the last fill, kVerbCount before the first one.
|
||||
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
const MGPipeFilledState& FilledState() const { return m_filled; }
|
||||
#endif
|
||||
|
||||
// ---- V: values ----
|
||||
Int GetActiveTextureUnit() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetActiveTextureUnit);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetActiveTextureUnit, 0, 0);
|
||||
return m_activeTextureUnit;
|
||||
}
|
||||
const FloatVec4& GetBlendColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendColor, 0, 0);
|
||||
return m_blendColor;
|
||||
}
|
||||
void GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendEquationIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendEquationIndexed, index, 0);
|
||||
if (index >= kMGMaxDrawBuffers) {
|
||||
MOBILEGL_ASSERT(false, "Blend equation index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
color = m_blendEquation[index][0];
|
||||
alpha = m_blendEquation[index][1];
|
||||
}
|
||||
void GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
|
||||
BlendFactor& dstAlpha) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendFuncIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendFuncIndexed, index, 0);
|
||||
if (index >= kMGMaxDrawBuffers) {
|
||||
MOBILEGL_ASSERT(false, "Blend func index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
srcRGB = m_blendFunc[index][0];
|
||||
dstRGB = m_blendFunc[index][1];
|
||||
srcAlpha = m_blendFunc[index][2];
|
||||
dstAlpha = m_blendFunc[index][3];
|
||||
}
|
||||
// Dead field: filled, read by no backend since the D21 XFB counter-slot rekey; kept so
|
||||
// the vendored inventory row keeps its mapping (Coverage.def).
|
||||
Uint GetBoundTransformFeedbackName() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackName);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackName, 0, 0);
|
||||
return m_boundTransformFeedbackName;
|
||||
}
|
||||
SizeT GetTouchedBufferBindingPointCount(BufferTarget target) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTouchedBufferBindingPointCount);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTouchedBufferBindingPointCount, static_cast<Uint>(target), 0);
|
||||
return m_touchedBindingPointCount[static_cast<SizeT>(target)];
|
||||
}
|
||||
GLenum GetClampReadColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClampReadColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClampReadColor, 0, 0);
|
||||
return m_clampReadColor;
|
||||
}
|
||||
const FloatVec4& GetClearColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearColor, 0, 0);
|
||||
return m_clearColor;
|
||||
}
|
||||
Float GetClearDepth() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearDepth);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearDepth, 0, 0);
|
||||
return m_clearDepth;
|
||||
}
|
||||
Uint32 GetClearStencil() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearStencil);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearStencil, 0, 0);
|
||||
return m_clearStencil;
|
||||
}
|
||||
BoolVec4 GetColorMaskIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetColorMaskIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetColorMaskIndexed, index, 0);
|
||||
return m_colorMask[index];
|
||||
}
|
||||
CullFaceMode GetCullFaceMode() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetCullFaceMode);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCullFaceMode, 0, 0);
|
||||
return m_cullFaceMode;
|
||||
}
|
||||
const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetCurrentVertexAttribute);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCurrentVertexAttribute, index, 0);
|
||||
if (index >= kMaxVertexAttribs) {
|
||||
static const CurrentVertexAttributeValue defaultValue{};
|
||||
MGLOG_E_ONCE("PipeInputs::GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
return defaultValue;
|
||||
}
|
||||
return m_currentVertexAttribute[index];
|
||||
}
|
||||
DepthTestFunc GetDepthFunc() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthFunc);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthFunc, 0, 0);
|
||||
return m_depthFunc;
|
||||
}
|
||||
Bool GetDepthMask() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthMask);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthMask, 0, 0);
|
||||
return m_depthMask;
|
||||
}
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthRangeIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthRangeIndexed, index, 0);
|
||||
if (index >= kMaxViewports) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_depthRange[0];
|
||||
}
|
||||
return m_depthRange[index];
|
||||
}
|
||||
Float GetLineWidth() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetLineWidth);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLineWidth, 0, 0);
|
||||
return m_lineWidth;
|
||||
}
|
||||
LogicOperation GetLogicOp() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetLogicOp);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLogicOp, 0, 0);
|
||||
return m_logicOp;
|
||||
}
|
||||
Int GetMaxTouchedTextureUnit() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetMaxTouchedTextureUnit);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMaxTouchedTextureUnit, 0, 0);
|
||||
return m_maxTouchedTextureUnit;
|
||||
}
|
||||
Float GetMinSampleShadingValue() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetMinSampleShadingValue);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMinSampleShadingValue, 0, 0);
|
||||
return m_minSampleShadingValue;
|
||||
}
|
||||
const FloatVec2& GetPatchDefaultInnerLevel() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultInnerLevel);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultInnerLevel, 0, 0);
|
||||
return m_patchDefaultInnerLevel;
|
||||
}
|
||||
const FloatVec4& GetPatchDefaultOuterLevel() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultOuterLevel);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultOuterLevel, 0, 0);
|
||||
return m_patchDefaultOuterLevel;
|
||||
}
|
||||
Uint GetPatchVertices() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchVertices);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchVertices, 0, 0);
|
||||
return m_patchVertices;
|
||||
}
|
||||
Uint GetPipelineStateVersion() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPipelineStateVersion);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPipelineStateVersion, 0, 0);
|
||||
return m_pipelineStateVersion;
|
||||
}
|
||||
Uint GetRenderStateParametersVersion() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParametersVersion);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
|
||||
return m_renderStateParametersVersion;
|
||||
}
|
||||
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
|
||||
return m_pixelStore[isUnpack ? 1 : 0];
|
||||
}
|
||||
GLenum GetPolygonModeFront() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonModeFront);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonModeFront, 0, 0);
|
||||
return m_polygonModeFront;
|
||||
}
|
||||
Float GetPolygonOffsetFactor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetFactor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetFactor, 0, 0);
|
||||
return m_polygonOffsetFactor;
|
||||
}
|
||||
Float GetPolygonOffsetUnits() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetUnits);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetUnits, 0, 0);
|
||||
return m_polygonOffsetUnits;
|
||||
}
|
||||
Uint32 GetPrimitiveRestartIndex() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPrimitiveRestartIndex);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPrimitiveRestartIndex, 0, 0);
|
||||
return m_primitiveRestartIndex;
|
||||
}
|
||||
ProvokingVertexMode GetProvokingVertexMode() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProvokingVertexMode);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProvokingVertexMode, 0, 0);
|
||||
return m_provokingVertexMode;
|
||||
}
|
||||
const RenderStateParameters& GetRenderStateParameters() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParameters);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParameters, 0, 0);
|
||||
return m_renderState;
|
||||
}
|
||||
Uint64 GetSamplingResolutionGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
|
||||
return m_samplingResolutionGeneration;
|
||||
}
|
||||
const IntVec4& GetScissorBox() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetScissorBox);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetScissorBox, 0, 0);
|
||||
return m_scissorBox;
|
||||
}
|
||||
const StencilFaceState& GetStencilState(StencilFace face) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetStencilState);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetStencilState, static_cast<Uint>(face), 0);
|
||||
return m_stencil[face == StencilFace::Back ? 1 : 0];
|
||||
}
|
||||
Uint64 GetTextureBindGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
|
||||
return m_textureBindGeneration;
|
||||
}
|
||||
Uint64 GetTextureContextId() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
|
||||
return m_textureContextId;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackCapturedVertices);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackCapturedVertices, 0, 0);
|
||||
return m_transformFeedbackCapturedVertices;
|
||||
}
|
||||
Uint64 GetTransformFeedbackGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackGeneration, 0, 0);
|
||||
return m_transformFeedbackGeneration;
|
||||
}
|
||||
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter, 0, 0);
|
||||
return m_transformFeedbackPausedPrimitiveCounter;
|
||||
}
|
||||
Uint64 GetBoundTransformFeedbackLifetimeId() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackLifetimeId);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackLifetimeId, 0, 0);
|
||||
return m_boundTransformFeedbackLifetimeId;
|
||||
}
|
||||
IntVec4 GetViewport() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetViewport);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewport, 0, 0);
|
||||
return m_viewport;
|
||||
}
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetViewportIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewportIndexed, index, 0);
|
||||
if (index >= kMaxViewports) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_viewportIndexed[0];
|
||||
}
|
||||
return m_viewportIndexed[index];
|
||||
}
|
||||
Bool IsCapabilityEnabled(CapabilityInput cap) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabled);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabled, static_cast<Uint>(cap), 0);
|
||||
const auto index = static_cast<SizeT>(cap);
|
||||
return index < kCapabilityCount ? m_capability[index] : false;
|
||||
}
|
||||
// Blend and ScissorTest are the only indexed capabilities GLContext keeps; no backend
|
||||
// asks for another (VulkanRenderer asks Blend). Any other cap is a read the fill cannot
|
||||
// have served: Fatal{UnmigratedPipeInput} naming the field and the verb, the cap in a
|
||||
// preceding MGLOG_E.
|
||||
Bool IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabledIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabledIndexed, static_cast<Uint>(cap), index);
|
||||
if (cap == CapabilityInput::Blend) {
|
||||
return index < kMGMaxDrawBuffers ? m_capabilityIndexed.Blend[index] : false;
|
||||
}
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
return index < kMaxViewports ? m_capabilityIndexed.ScissorTest[index] : false;
|
||||
}
|
||||
MGLOG_E("PipeInputs::IsCapabilityEnabledIndexed: no indexed storage for cap=%d (index=%u)",
|
||||
static_cast<int>(cap), index);
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::IsCapabilityEnabledIndexed, m_currentVerb);
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackActive);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackActive, 0, 0);
|
||||
return m_transformFeedbackActive;
|
||||
}
|
||||
Bool IsTransformFeedbackPaused() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackPaused);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackPaused, 0, 0);
|
||||
return m_transformFeedbackPaused;
|
||||
}
|
||||
|
||||
// ---- O: object references ----
|
||||
const SharedPtr<VertexArrayObject>& GetBoundVertexArray() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundVertexArray);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundVertexArray, 0, 0);
|
||||
return m_boundVertexArray;
|
||||
}
|
||||
// A target the fill left null (one outside GlobalBufferTargets / BufferBindPointTargets,
|
||||
// or a read before any fill) is a read the fill cannot have served: the poison Fatal,
|
||||
// the target in a preceding MGLOG_E.
|
||||
BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget target) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast<Uint>(target), 0);
|
||||
const auto index = static_cast<SizeT>(target);
|
||||
if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast<int>(target));
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb);
|
||||
}
|
||||
return *m_bufferBindingSlot[index];
|
||||
}
|
||||
BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget target, Uint index) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast<Uint>(target), index);
|
||||
const auto targetIndex = static_cast<SizeT>(target);
|
||||
if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)",
|
||||
static_cast<int>(target), index);
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb);
|
||||
}
|
||||
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
|
||||
// (BufferState.h), so base[index] is the live slot GLContext would hand out.
|
||||
return m_bufferBindingPointBase[targetIndex][index];
|
||||
}
|
||||
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast<Uint>(target), 0);
|
||||
const auto index = static_cast<SizeT>(target);
|
||||
if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast<int>(target));
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetFramebufferBindingSlot, m_currentVerb);
|
||||
}
|
||||
return *m_framebufferBindingSlot[index];
|
||||
}
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
|
||||
if (m_imageTextureBindingBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
|
||||
}
|
||||
return m_imageTextureBindingBase[unit];
|
||||
}
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
|
||||
if (m_imageTextureBindingBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
|
||||
}
|
||||
return m_imageTextureBindingBase[unit];
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetProgramForDispatch() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0);
|
||||
return m_programForDispatch;
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetProgramForDraw() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0);
|
||||
return m_programForDraw;
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackProgram);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackProgram, 0, 0);
|
||||
return m_transformFeedbackProgram;
|
||||
}
|
||||
TextureUnit& GetTextureUnitObject(Int unit) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureUnitObject);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureUnitObject, static_cast<Uint>(unit), 0);
|
||||
if (m_textureUnitBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetTextureUnitObject, m_currentVerb);
|
||||
}
|
||||
return m_textureUnitBase[unit];
|
||||
}
|
||||
|
||||
// ---- F: forwarded to the live context (MG_Impl/Pipe/PipeFill.cpp); sticky ----
|
||||
// Each takes an argument that is not verb state - a GL name, a lifetime id, a target -
|
||||
// i.e. it is a lookup or a reverse-channel write, not a state read; there is no value
|
||||
// the filler could copy and no verb whose fill could make it stale. Phase C replaces
|
||||
// them with handle tables and callbacks.
|
||||
// They carry no MGP_INPUT_CHECK / MGP_INPUT_VERIFY_READ (the declared exception to
|
||||
// P1 brief D4's "every accessor body"): a forward is a live call, not a stored value,
|
||||
// and InvalidateCompileEnv is reached from backend initialisation before any verb has
|
||||
// filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp
|
||||
// is therefore consulted by no accessor; the tests pin it through
|
||||
// MGPipeInputFieldIsFresh directly.
|
||||
SizeT GetBufferBindingPointCount(BufferTarget target) const;
|
||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
|
||||
void InvalidateCompileEnv();
|
||||
Bool ValidateProgramName(Uint index) const;
|
||||
// Dropped with an MGLOG_E_ONCE when no context is live; today's guarded sites never
|
||||
// reach it without one.
|
||||
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
|
||||
|
||||
// ---- the storage visitor ----
|
||||
// Calls fn(a.<member>, b.<member>) for the field's storage and returns its result; returns
|
||||
// false without calling fn for a forwarded field, which has none. The comparator's
|
||||
// per-field equality and the verify corruption injector are both one call of this.
|
||||
template <class Fn>
|
||||
static Bool VisitStorage(MGPipeInputField field, PipeInputs& a, PipeInputs& b, Fn&& fn) {
|
||||
switch (field) {
|
||||
#define MGP_INPUT_VISIT(Field, Member) \
|
||||
case MGPipeInputField::Field: \
|
||||
return fn(a.Member, b.Member);
|
||||
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
|
||||
#undef MGP_INPUT_VISIT
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
template <class Fn>
|
||||
static Bool VisitStorage(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b, Fn&& fn) {
|
||||
switch (field) {
|
||||
#define MGP_INPUT_VISIT(Field, Member) \
|
||||
case MGPipeInputField::Field: \
|
||||
return fn(a.Member, b.Member);
|
||||
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
|
||||
#undef MGP_INPUT_VISIT
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
|
||||
// the filler's per-field copies and stamps, and the verify snapshot.
|
||||
friend struct MGPipeFillAccess;
|
||||
|
||||
// ---- identity ----
|
||||
const void* m_contextIdentity = nullptr;
|
||||
Bool m_live = false;
|
||||
MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState m_filled{};
|
||||
#endif
|
||||
|
||||
// ---- V ----
|
||||
Int m_activeTextureUnit = 0;
|
||||
FloatVec4 m_blendColor{};
|
||||
BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]{};
|
||||
BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]{};
|
||||
Uint m_boundTransformFeedbackName = 0;
|
||||
SizeT m_touchedBindingPointCount[kBufferTargetCount]{};
|
||||
GLenum m_clampReadColor = 0;
|
||||
FloatVec4 m_clearColor{};
|
||||
Float m_clearDepth = 0.f;
|
||||
Uint32 m_clearStencil = 0;
|
||||
BoolVec4 m_colorMask[kMGMaxDrawBuffers]{};
|
||||
CullFaceMode m_cullFaceMode{};
|
||||
CurrentVertexAttributeValue m_currentVertexAttribute[kMaxVertexAttribs]{};
|
||||
DepthTestFunc m_depthFunc{};
|
||||
Bool m_depthMask = false;
|
||||
FloatVec2 m_depthRange[kMaxViewports]{};
|
||||
Float m_lineWidth = 0.f;
|
||||
LogicOperation m_logicOp{};
|
||||
Int m_maxTouchedTextureUnit = -1;
|
||||
Float m_minSampleShadingValue = 0.f;
|
||||
FloatVec2 m_patchDefaultInnerLevel{};
|
||||
FloatVec4 m_patchDefaultOuterLevel{};
|
||||
Uint m_patchVertices = 0;
|
||||
Uint m_pipelineStateVersion = 0;
|
||||
Uint m_renderStateParametersVersion = 0;
|
||||
PixelStoreParameters m_pixelStore[2]{}; // [0] = pack, [1] = unpack
|
||||
GLenum m_polygonModeFront = 0;
|
||||
Float m_polygonOffsetFactor = 0.f;
|
||||
Float m_polygonOffsetUnits = 0.f;
|
||||
Uint32 m_primitiveRestartIndex = 0;
|
||||
ProvokingVertexMode m_provokingVertexMode{};
|
||||
RenderStateParameters m_renderState{};
|
||||
Uint64 m_samplingResolutionGeneration = 0;
|
||||
Uint64 m_textureBindGeneration = 0;
|
||||
Uint64 m_textureContextId = 0;
|
||||
IntVec4 m_scissorBox{};
|
||||
StencilFaceState m_stencil[kStencilFaceCount]{};
|
||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||
Uint64 m_transformFeedbackGeneration = 0;
|
||||
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
|
||||
Uint64 m_boundTransformFeedbackLifetimeId = 0;
|
||||
IntVec4 m_viewport{};
|
||||
FloatVec4 m_viewportIndexed[kMaxViewports]{};
|
||||
Bool m_capability[kCapabilityCount]{};
|
||||
IndexedCapabilities m_capabilityIndexed{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
Bool m_transformFeedbackPaused = false;
|
||||
|
||||
// ---- O ----
|
||||
SharedPtr<VertexArrayObject> m_boundVertexArray;
|
||||
BindingSlot<BufferObject>* m_bufferBindingSlot[kBufferTargetCount]{};
|
||||
BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[kBufferTargetCount]{};
|
||||
BindingSlot<FramebufferObject>* m_framebufferBindingSlot[kFramebufferTargetCount]{};
|
||||
ImageTextureBinding* m_imageTextureBindingBase = nullptr;
|
||||
SharedPtr<ProgramObject> m_programForDispatch;
|
||||
SharedPtr<ProgramObject> m_programForDraw;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
TextureUnit* m_textureUnitBase = nullptr;
|
||||
};
|
||||
|
||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||
// variable: no .cpp is needed for the definition.
|
||||
inline PipeInputs gPipeInputs{};
|
||||
|
||||
// Every field has storage or is forwarded, and nothing else.
|
||||
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
||||
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
|
||||
"MGP_INPUT_STORAGE_LIST plus the seven forwarded fields is not the PipeInputs field set");
|
||||
#undef MGP_INPUT_COUNT_ONE
|
||||
// The docs budget ~20 KB; the block is a few KB.
|
||||
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
|
||||
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
|
||||
// always equal (no storage).
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b);
|
||||
// PipeInputs.cpp. The entry compare: every field in `mask` of the pushed block against the
|
||||
// snapshot, first differing field out. Exported from the shared library on purpose - the
|
||||
// retrace-verify CI job proves it swapped in a verify build by finding this symbol with
|
||||
// nm -D, so a "green" run against a library without the comparator cannot happen.
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField);
|
||||
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
|
||||
// scalar, ^0x5A the first byte of a struct, flip a pointer's low bits - never
|
||||
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
|
||||
// which has nothing to corrupt.
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,271 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Debug.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
|
||||
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
|
||||
// off the query and then trips a different limit here would have no way to explain it.
|
||||
constexpr SizeT kMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLsizei kMaxDebugMessageLength = 1024;
|
||||
constexpr GLsizei kMaxLabelLength = 256;
|
||||
|
||||
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
|
||||
// nothing else in MobileGL reads it, and it is keyed on the context id so a
|
||||
// destroyed-and-recreated context starts with an empty stack and no labels - which the
|
||||
// unit tests, which recreate the context between cases, depend on.
|
||||
struct DebugState {
|
||||
Uint64 contextId = 0;
|
||||
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
|
||||
// the context with is implicit and is what makes the reported depth start at 1.
|
||||
Vector<String> groupStack;
|
||||
// Keyed by (identifier, name); see MakeObjectLabelKey.
|
||||
UnorderedMap<Uint64, String> objectLabels;
|
||||
};
|
||||
|
||||
DebugState& State() {
|
||||
static DebugState state;
|
||||
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
|
||||
if (state.contextId != contextId) {
|
||||
state.contextId = contextId;
|
||||
state.groupStack.clear();
|
||||
state.objectLabels.clear();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
|
||||
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
|
||||
}
|
||||
|
||||
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
|
||||
}
|
||||
|
||||
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
|
||||
// are reserved for the implementation itself.
|
||||
Bool ValidateInjectedSource(GLenum source, const char* caller) {
|
||||
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidEnum, caller,
|
||||
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
|
||||
"GL_DEBUG_SOURCE_THIRD_PARTY.",
|
||||
MG_Util::ConvertGLEnumToString(source)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
|
||||
// every one of these entry points spells "just use the whole thing".
|
||||
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
|
||||
const char* what) {
|
||||
const GLsizei effective =
|
||||
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
|
||||
if (effective < limit) {
|
||||
return true;
|
||||
}
|
||||
RecordDebugError(ErrorCode::InvalidValue, caller,
|
||||
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
|
||||
limit));
|
||||
return false;
|
||||
}
|
||||
|
||||
String MakeDebugString(GLsizei length, const GLchar* text) {
|
||||
if (text == nullptr) return {};
|
||||
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
|
||||
}
|
||||
|
||||
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
|
||||
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
|
||||
// has a frontend name check - so this is answered exactly rather than waved through.
|
||||
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
|
||||
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
|
||||
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
|
||||
outIdentifierKnown = true;
|
||||
auto* context = MG_State::pGLContext.get();
|
||||
switch (identifier) {
|
||||
case GL_BUFFER:
|
||||
return context->ValidateBufferName(name);
|
||||
case GL_SHADER:
|
||||
return context->ValidateShaderName(name);
|
||||
case GL_PROGRAM:
|
||||
return context->ValidateProgramName(name);
|
||||
case GL_VERTEX_ARRAY:
|
||||
return context->ValidateVertexArrayName(name);
|
||||
case GL_QUERY:
|
||||
return IsQuery(name) == GL_TRUE;
|
||||
case GL_PROGRAM_PIPELINE:
|
||||
return context->ValidateProgramPipelineName(name);
|
||||
case GL_TRANSFORM_FEEDBACK:
|
||||
return context->ValidateTransformFeedbackName(name);
|
||||
case GL_SAMPLER:
|
||||
return context->ValidateSamplerName(name);
|
||||
case GL_TEXTURE:
|
||||
return context->ValidateTextureName(name);
|
||||
case GL_RENDERBUFFER:
|
||||
return context->ValidateRenderbufferName(name);
|
||||
case GL_FRAMEBUFFER:
|
||||
// Name 0 is the default framebuffer, which is a real, labellable object.
|
||||
return name == 0 || context->ValidateFramebufferName(name);
|
||||
default:
|
||||
outIdentifierKnown = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetDebugGroupStackDepth() {
|
||||
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
|
||||
// reported depth is one more than the number of pushes the application has made.
|
||||
return static_cast<GLint>(State().groupStack.size()) + 1;
|
||||
}
|
||||
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
auto& state = State();
|
||||
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
|
||||
// Not INVALID_*: KHR_debug gives the group stack its own error code.
|
||||
RecordDebugError(ErrorCode::StackOverflow, __func__,
|
||||
std::format("the debug group stack is already {} deep, which is its maximum.",
|
||||
kMaxDebugGroupStackDepth));
|
||||
return;
|
||||
}
|
||||
state.groupStack.push_back(MakeDebugString(length, message));
|
||||
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
|
||||
}
|
||||
|
||||
void PopDebugGroup() {
|
||||
auto& state = State();
|
||||
if (state.groupStack.empty()) {
|
||||
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
|
||||
RecordDebugError(ErrorCode::StackUnderflow, __func__,
|
||||
"the debug group stack holds only the group the context was created with.");
|
||||
return;
|
||||
}
|
||||
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
|
||||
state.groupStack.pop_back();
|
||||
}
|
||||
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf) {
|
||||
static_cast<void>(id);
|
||||
if (!ValidateInjectedSource(source, __func__)) return;
|
||||
switch (type) {
|
||||
case GL_DEBUG_TYPE_ERROR:
|
||||
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
|
||||
case GL_DEBUG_TYPE_PORTABILITY:
|
||||
case GL_DEBUG_TYPE_PERFORMANCE:
|
||||
case GL_DEBUG_TYPE_MARKER:
|
||||
case GL_DEBUG_TYPE_PUSH_GROUP:
|
||||
case GL_DEBUG_TYPE_POP_GROUP:
|
||||
case GL_DEBUG_TYPE_OTHER:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("type {} is not a debug message type.",
|
||||
MG_Util::ConvertGLEnumToString(type)));
|
||||
return;
|
||||
}
|
||||
switch (severity) {
|
||||
case GL_DEBUG_SEVERITY_HIGH:
|
||||
case GL_DEBUG_SEVERITY_MEDIUM:
|
||||
case GL_DEBUG_SEVERITY_LOW:
|
||||
case GL_DEBUG_SEVERITY_NOTIFICATION:
|
||||
break;
|
||||
default:
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("severity {} is not a debug message severity.",
|
||||
MG_Util::ConvertGLEnumToString(severity)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
|
||||
|
||||
// No callback is ever invoked and the message log is empty by construction
|
||||
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
|
||||
// application-visible effect is exactly the error checking above. The text still reaches
|
||||
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
|
||||
// level, so an application that inserts a message per draw costs nothing in a release build.
|
||||
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
|
||||
}
|
||||
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
|
||||
|
||||
auto& labels = State().objectLabels;
|
||||
const Uint64 key = MakeObjectLabelKey(identifier, name);
|
||||
if (label == nullptr) {
|
||||
// GL 4.6 core 20.5: a NULL label removes any label the object had.
|
||||
labels.erase(key);
|
||||
return;
|
||||
}
|
||||
labels[key] = MakeDebugString(length, label);
|
||||
}
|
||||
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
if (bufSize < 0) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
|
||||
return;
|
||||
}
|
||||
Bool identifierKnown = false;
|
||||
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
|
||||
if (!identifierKnown) {
|
||||
RecordDebugError(ErrorCode::InvalidEnum, __func__,
|
||||
std::format("identifier {} is not a labellable object type.",
|
||||
MG_Util::ConvertGLEnumToString(identifier)));
|
||||
return;
|
||||
}
|
||||
if (!objectExists) {
|
||||
RecordDebugError(ErrorCode::InvalidValue, __func__,
|
||||
std::format("{} {} is not the name of an existing object.",
|
||||
MG_Util::ConvertGLEnumToString(identifier), name));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& labels = State().objectLabels;
|
||||
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
|
||||
const String& text = it != labels.end() ? it->second : String{};
|
||||
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
|
||||
// back an empty string with length 0 rather than an error.
|
||||
SizeT copied = 0;
|
||||
if (label != nullptr && bufSize > 0) {
|
||||
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
|
||||
std::memcpy(label, text.data(), copied);
|
||||
label[copied] = '\0';
|
||||
}
|
||||
if (length != nullptr) {
|
||||
*length = static_cast<GLsizei>(copied);
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
|
||||
// and to name their objects; Better Clouds calls all four for exactly that.
|
||||
//
|
||||
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
|
||||
// the host driver. Two independent reasons:
|
||||
//
|
||||
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
|
||||
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
|
||||
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
|
||||
// labelling.
|
||||
// * A debug GROUP is only meaningful if it brackets the commands the application issued
|
||||
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
|
||||
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
|
||||
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
|
||||
// wrong commands, which is a misleading capture rather than a helpful one.
|
||||
//
|
||||
// What the application can rely on is the observable contract: the group stack depth is real
|
||||
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
|
||||
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
|
||||
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
|
||||
void PopDebugGroup();
|
||||
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
||||
const GLchar* buf);
|
||||
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
|
||||
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
|
||||
|
||||
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
|
||||
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
|
||||
GLint GetDebugGroupStackDepth();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
@@ -34,8 +35,81 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
|
||||
// Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
|
||||
// is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
|
||||
// backend samples during this draw describes the program it is drawing), so the draw funnel
|
||||
// below resolves it exactly once and hands it to both users.
|
||||
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const char* functionName) {
|
||||
// "If there is no current program object or bound program pipeline object, the results of
|
||||
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
|
||||
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
|
||||
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
|
||||
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
|
||||
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
|
||||
// the compute shader stage"), which is why this lives on the draw path and not in the
|
||||
// shared ValidateProgramForExecution below.
|
||||
if (!currentProgram) return false;
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
|
||||
// inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
|
||||
// evaluation or geometry stage has an executable but no program supplies an executable
|
||||
// VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
|
||||
// shape - so in practice it catches a program pipeline assembled out of stage programs,
|
||||
// which today draws happily and renders nothing.
|
||||
//
|
||||
// Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
|
||||
// program is the graphics composite, whose linked-shader snapshot is built out of exactly
|
||||
// the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
|
||||
// stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
|
||||
// deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
|
||||
// nothing else here should start refusing draws GL accepts.
|
||||
//
|
||||
// On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
|
||||
// which shares that helper and legitimately has no vertex stage - is untouched.
|
||||
const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
||||
if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
|
||||
// default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
|
||||
// for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
|
||||
// drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
|
||||
// rules out baking it at link time.
|
||||
//
|
||||
// Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
|
||||
// decides the value and either half can move between draws. It costs a phase-A flag read for
|
||||
// every program that has no shim, and a 4-byte compare for the ones that do: the write only
|
||||
// bumps the UBO content version when the number actually changes, so a run of draws into one
|
||||
// framebuffer re-uploads nothing.
|
||||
static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
||||
if (!program || !program->UsesReservedNumSamples()) return;
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
|
||||
// when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
|
||||
}
|
||||
|
||||
// The one funnel every drawing command passes through. Order is load-bearing: validate first
|
||||
// (a rejected draw must leave state alone), then publish the sample count - which reads the
|
||||
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
|
||||
// and before the backend consumes the program's UBO content version.
|
||||
static Bool PrepareCurrentProgramForDraw(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
|
||||
PublishDrawFramebufferSampleCount(currentProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
|
||||
@@ -73,6 +147,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
|
||||
// Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
|
||||
// them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
|
||||
// line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
|
||||
// the default arm did - made AccountTransformFeedbackPrimitives bail before it had
|
||||
// recorded anything, so an adjacency capture advanced neither the captured-vertex
|
||||
// counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
|
||||
// that routes the transform feedback queries to the driver's own counter.
|
||||
case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
|
||||
case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
|
||||
case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
|
||||
// GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
|
||||
// on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
// to the driver's own counter, which is the only correct source for a patch capture.
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -99,11 +187,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_LINES:
|
||||
case GL_LINE_STRIP:
|
||||
case GL_LINE_LOOP:
|
||||
// An adjacency primitive delivers the same line/triangle to the geometry stage; the
|
||||
// adjacent vertices are context, not part of the primitive.
|
||||
case GL_LINES_ADJACENCY:
|
||||
case GL_LINE_STRIP_ADJACENCY:
|
||||
verticesPerPrimitive = 2;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN:
|
||||
case GL_TRIANGLES_ADJACENCY:
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||
verticesPerPrimitive = 3;
|
||||
break;
|
||||
default:
|
||||
@@ -308,11 +402,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POINTS:
|
||||
compatible = mode == GL_POINTS;
|
||||
break;
|
||||
// The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
|
||||
// This arm is only reached when the program has NO geometry or tessellation
|
||||
// evaluation stage, and without a geometry stage the adjacent vertices are simply
|
||||
// ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
|
||||
// so the combination is legal and must capture. Omitting them raised a spurious
|
||||
// GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
|
||||
// with its pre-draw bytes. The geometry-stage input table above already carries the
|
||||
// same four arms; this is the second table catching up with it.
|
||||
case GL_LINES:
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
|
||||
mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
|
||||
mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -382,6 +486,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an
|
||||
// INVALID_VALUE error is generated". The same sentence covers instancecount and the
|
||||
// MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the
|
||||
// caller so the message says which argument the application actually got wrong.
|
||||
static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) {
|
||||
if (value >= 0) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(argumentName) + " must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is
|
||||
// generated". Both are uints, so a caller that passes -1 for start arrives here as
|
||||
// 0xFFFFFFFF and is caught by the same comparison - which is exactly what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks.
|
||||
static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) {
|
||||
if (end >= start) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "end must not be less than start."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
||||
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
||||
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
||||
@@ -399,6 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(Clear);
|
||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||
}
|
||||
|
||||
@@ -407,6 +537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -416,6 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||
}
|
||||
|
||||
@@ -425,6 +557,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -434,6 +567,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -442,6 +576,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||
}
|
||||
|
||||
@@ -451,6 +586,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
@@ -460,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -468,6 +605,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -477,6 +615,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||
maxdrawcount, stride);
|
||||
}
|
||||
@@ -487,6 +626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||
stride);
|
||||
}
|
||||
@@ -497,6 +637,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||
basevertex);
|
||||
}
|
||||
@@ -507,6 +648,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -517,6 +659,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||
}
|
||||
@@ -527,6 +670,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstancedBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -537,6 +681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||
instancecount, baseinstance);
|
||||
}
|
||||
@@ -547,6 +692,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
@@ -555,6 +701,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||
}
|
||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
@@ -563,6 +710,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||
baseinstance);
|
||||
}
|
||||
@@ -572,6 +720,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
@@ -580,6 +729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||
}
|
||||
|
||||
@@ -611,6 +761,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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;
|
||||
MGP_FILL(DispatchCompute);
|
||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
@@ -663,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -684,11 +836,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
|
||||
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
|
||||
MGP_FILL(PatchParameteri);
|
||||
patchParameteri(pname, value);
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
|
||||
// NO control stage tessellates at; both backends have to synthesize that control stage
|
||||
// themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
|
||||
// there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
|
||||
// pname is the only error the spec lists: any float values are accepted, negatives and NaN
|
||||
// included, and it is the tessellator that clamps them.
|
||||
//
|
||||
// This used to be a stub, which is why the two synthesizers hardcoded 1.0.
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values) {
|
||||
if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
|
||||
return;
|
||||
}
|
||||
if (!values) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
|
||||
return;
|
||||
}
|
||||
if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
|
||||
MG_State::pGLContext->SetPatchDefaultOuterLevel(
|
||||
FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else {
|
||||
MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
|
||||
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
|
||||
// GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
|
||||
// Forwarding an undefined bit to the host driver let a caller that had computed its mask
|
||||
// wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
|
||||
constexpr GLbitfield kAllDefinedBarrierBits =
|
||||
GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
|
||||
GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
|
||||
GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
|
||||
GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
|
||||
GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;
|
||||
|
||||
Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
|
||||
if (barriers == GL_ALL_BARRIER_BITS) return true;
|
||||
if ((barriers & ~kAllDefinedBarrierBits) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
|
||||
"barriers contains bits that are not defined barrier bits."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MemoryBarrier(GLbitfield barriers) {
|
||||
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
|
||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||
if (!memoryBarrier) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -696,10 +907,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
void TextureBarrier() {
|
||||
// GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
|
||||
// framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
|
||||
// read texels of a texture that is also attached to the current framebuffer.
|
||||
//
|
||||
// Both backends serve this through their existing memory-barrier hook rather than a new
|
||||
// entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
|
||||
// writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
|
||||
// is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
|
||||
// scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
|
||||
// subset, so it cannot under-synchronize.
|
||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||
if (!memoryBarrier) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
}
|
||||
|
||||
void MemoryBarrierByRegion(GLbitfield barriers) {
|
||||
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
|
||||
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
|
||||
if (!memoryBarrierByRegion) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -708,19 +943,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support regional memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrierByRegion);
|
||||
memoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||
}
|
||||
@@ -798,7 +1034,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
|
||||
// they produce for a call made with no program bound. Same precedence decision, and
|
||||
// the same reason, as DispatchComputeIndirect above.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||
if (!multiDrawElementsIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -819,7 +1055,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
// See MultiDrawElementsIndirectCount, including why this one goes last.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||
if (!multiDrawArraysIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -834,14 +1070,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateDrawElementsRange(__func__, start, end)) return;
|
||||
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
||||
}
|
||||
@@ -849,7 +1088,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||
baseinstance);
|
||||
@@ -858,29 +1097,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return;
|
||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
|
||||
@@ -890,21 +1132,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||
DrawArraysIndirect_Backend(mode, indirect);
|
||||
@@ -912,15 +1154,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
@@ -928,7 +1172,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (drawcount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -942,7 +1186,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||
}
|
||||
@@ -950,8 +1194,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
|
||||
// GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate
|
||||
// DrawElementsBaseVertex calls, so each element of the count array carries the same
|
||||
// non-negative requirement the single-draw entry point applies to its own count. The
|
||||
// whole call is rejected before any sub-draw is issued, which is what makes the error
|
||||
// observable at all - a driver that drew the valid prefix first would leave the
|
||||
// framebuffer half-written.
|
||||
if (count != nullptr) {
|
||||
for (GLsizei draw = 0; draw < drawcount; ++draw) {
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return;
|
||||
}
|
||||
}
|
||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
@@ -961,7 +1218,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
@@ -1009,6 +1266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||
MGP_FILL(BeginTransformFeedback);
|
||||
beginXfb(primitiveMode);
|
||||
}
|
||||
}
|
||||
@@ -1091,6 +1349,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Closed while the capture state is still active: a backend that captures
|
||||
// through its own driver reads the capture program and buffer bindings here.
|
||||
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||
MGP_FILL(EndTransformFeedback);
|
||||
endXfb();
|
||||
}
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
@@ -1099,9 +1358,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// the GPU work is all that is required.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
MGP_FILL(ClientWaitSync);
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
@@ -1120,6 +1382,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(true);
|
||||
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
|
||||
MGP_FILL(PauseTransformFeedback);
|
||||
pauseXfb();
|
||||
}
|
||||
}
|
||||
@@ -1134,6 +1397,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(false);
|
||||
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
|
||||
MGP_FILL(ResumeTransformFeedback);
|
||||
resumeXfb();
|
||||
}
|
||||
}
|
||||
@@ -1339,6 +1603,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
continue;
|
||||
}
|
||||
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
|
||||
MGP_FILL(DeleteTransformFeedback);
|
||||
deleteXfb(id);
|
||||
}
|
||||
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
|
||||
@@ -1370,6 +1635,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BindTransformFeedbackObject(id);
|
||||
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
|
||||
MGP_FILL(BindTransformFeedback);
|
||||
bindXfb(id);
|
||||
}
|
||||
}
|
||||
@@ -1385,7 +1651,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// (GL 4.6 core 10.3.7).
|
||||
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
|
||||
GLsizei instancecount) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return;
|
||||
if (!PrepareCurrentProgramForDraw(functionName)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
|
||||
if (instancecount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1408,8 +1674,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(id) + " is not a transform feedback object name."));
|
||||
return;
|
||||
}
|
||||
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
|
||||
if (stream != 0) {
|
||||
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
|
||||
// answers as 1 - so stream 0 is the only one that exists and anything else is
|
||||
// INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
|
||||
// never drift: if vertex-stream support ever lands, this bound moves with the limit.
|
||||
GLint maxVertexStreams = 1;
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
@@ -1427,6 +1698,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
|
||||
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
|
||||
if (vertices == 0) return;
|
||||
const auto count = static_cast<GLsizei>(vertices);
|
||||
|
||||
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void PatchParameteri(GLenum pname, GLint value);
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
void MemoryBarrierByRegion(GLbitfield barriers);
|
||||
void TextureBarrier();
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../Framebuffer/GL_Framebuffer.h"
|
||||
#include "../VertexArray/GL_VertexArray.h"
|
||||
#include "../Sync/GL_Sync.h"
|
||||
#include "../Debug/GL_Debug.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -159,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
|
||||
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
|
||||
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup)
|
||||
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
(void)length;
|
||||
(void)label;
|
||||
}
|
||||
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
|
||||
(void)identifier;
|
||||
(void)name;
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
if (label && bufSize > 0) {
|
||||
label[0] = '\0';
|
||||
}
|
||||
}
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
|
||||
@@ -424,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
||||
@@ -936,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
||||
@@ -996,7 +983,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||
@@ -1007,7 +994,7 @@ DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||
@@ -1060,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||
@@ -1120,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
|
||||
@@ -1163,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
|
||||
@@ -1848,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
@@ -2062,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
|
||||
@@ -2559,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -474,6 +475,75 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
|
||||
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
|
||||
// one another in that section and a CTS case that walks the family must not get five
|
||||
// different answers - which is exactly what happened when these lived in one helper that
|
||||
// only two of the five went through.
|
||||
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>&
|
||||
framebufferObject,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
|
||||
// than or equal to MAX_COLOR_ATTACHMENTS."
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
|
||||
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
|
||||
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
|
||||
// object is always there, and framebuffer 0 has to be recognised by identity instead,
|
||||
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
|
||||
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
|
||||
// from what the swapchain keeps publishing.
|
||||
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
|
||||
if (!framebufferObject ||
|
||||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"No framebuffer object is bound to the target; the default framebuffer's attachments "
|
||||
"cannot be named."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
|
||||
// texture with immutable storage it "must be smaller than the number of levels the texture
|
||||
// has". Split from the attachment-point half because the caller only has a texture object
|
||||
// once the detach (texture == 0) case is behind it.
|
||||
Bool ValidateFramebufferTextureLevel(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLint level) {
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Texture level must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (!textureObject || !textureObject->IsImmutable()) {
|
||||
// A mutable texture has no level bound here: a level it has not specified yet is
|
||||
// not an error, it just leaves the framebuffer incomplete.
|
||||
return true;
|
||||
}
|
||||
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
|
||||
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
|
||||
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
|
||||
// two-level view onto a ten-level texture would accept level 5 and attach an image
|
||||
// nothing can draw into.
|
||||
const Uint levelBound = textureObject->GetAddressableLevelCount();
|
||||
if (static_cast<Uint>(level) >= levelBound) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
|
||||
levelBound)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level,
|
||||
TextureUploadTarget textureUploadTarget, Bool layered = false) {
|
||||
@@ -482,10 +552,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
|
||||
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
|
||||
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
|
||||
// which half is being recorded - and dropping it here (the parameter defaults to
|
||||
// false) recorded a non-layered depth/stencil attachment beside a layered colour
|
||||
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
|
||||
// own rule, and downstream it means the depth/stencil attachment covers layer 0
|
||||
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
|
||||
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
|
||||
// attached one layer of it beside a layered colour target, which the driver
|
||||
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
|
||||
// produced nothing. This is the shape
|
||||
// texture_cube_map_array.stencil_attachments_*_layered and
|
||||
// geometry_shader.layered_framebuffer.stencil_support are built on.
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -497,13 +581,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -518,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -538,6 +617,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
MGP_FILL(BlitFramebuffer);
|
||||
MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||
mask, filter);
|
||||
}
|
||||
@@ -551,6 +631,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
MGP_FILL(BlitNamedFramebuffer);
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
dstY1, mask, filter);
|
||||
}
|
||||
@@ -562,6 +643,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ClearNamedFramebufferfv);
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -572,6 +654,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ClearNamedFramebufferfi);
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
@@ -582,6 +665,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ClearNamedFramebufferiv);
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -592,6 +676,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ClearNamedFramebufferuiv);
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -624,16 +709,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
|
||||
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||
// The multisample TEXTURE path resolves the limit per format the same way
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
|
||||
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
|
||||
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
|
||||
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
|
||||
// count it actually hands the driver.
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
|
||||
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
|
||||
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
|
||||
// integer path is genuinely one sample, and accepting four only moved the failure from an
|
||||
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
|
||||
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
|
||||
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
|
||||
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
|
||||
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
|
||||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
|
||||
return 0;
|
||||
}
|
||||
const auto& sampleCounts =
|
||||
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||
return sampleCounts.empty() ? 0 : sampleCounts.front();
|
||||
}
|
||||
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
@@ -644,13 +746,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
&normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
|
||||
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
|
||||
// supported for internalformat (see GetInternalformativ)", and
|
||||
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
|
||||
// list. It was never consulted here - the TODO that deferred it was written before the
|
||||
// query was backed and had gone stale - so a format whose multisample probes fail inside
|
||||
// a category that allows four was accepted at four, quietly allocated at one by
|
||||
// ClampSamplesToBackendSupport, and then reported as four by
|
||||
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
|
||||
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
|
||||
if (probedMaxSamples > 0) {
|
||||
return probedMaxSamples;
|
||||
}
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
|
||||
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
|
||||
// clamps to it, while the frontend honours what it advertised.
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
|
||||
return GetAdvertisedIntegerMaxSamples();
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
@@ -682,8 +795,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
|
||||
// glGetInternalformativ is backed; integer formats are handled below.
|
||||
// Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
|
||||
// to the format's category pname where nothing was probed. (This carried a TODO deferring
|
||||
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
|
||||
// both renderbuffers and multisample textures since, so the deferral was collected.)
|
||||
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
|
||||
if (samples > maxSamples) {
|
||||
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
|
||||
@@ -1048,13 +1163,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1069,6 +1178,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
if (layer < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -1191,6 +1301,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
// glFramebufferTexture2D is by far the most-used member of the family and the only one
|
||||
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
|
||||
// conditions have to be asked here explicitly.
|
||||
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
|
||||
attachmentType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1205,6 +1322,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -1241,6 +1359,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
|
||||
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
|
||||
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
|
||||
// every texture name that was never generated.
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1291,13 +1415,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State",
|
||||
"Texture level must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// The whole level condition, not just its negative half: glNamedFramebufferTexture and
|
||||
// glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
|
||||
// be rejected on both or a CTS case gets two answers for one rule.
|
||||
if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
|
||||
|
||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
|
||||
Bool layered = false;
|
||||
@@ -2615,24 +2736,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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;
|
||||
MGP_FILL(ClearBufferfi);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
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;
|
||||
MGP_FILL(ClearBufferfv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, 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;
|
||||
MGP_FILL(ClearBufferuiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, 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;
|
||||
MGP_FILL(ClearBufferiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -2880,6 +3005,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Getter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
@@ -27,6 +30,7 @@
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
@@ -92,8 +96,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// limits they advertise still have to be legal.
|
||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||
// The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
|
||||
// literal: they used to be independent (4096 components against 128 vectors, 64 varying
|
||||
// components against 8 varying vectors) and could not both be describing the same
|
||||
// capacity. Both are shared with BuildTBuiltInResource through Types.h, because
|
||||
// gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
|
||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxVertexOutputComponents = 64;
|
||||
constexpr GLint kFrontendMaxFragmentInputComponents = 128;
|
||||
@@ -105,21 +116,61 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
|
||||
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
|
||||
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks +
|
||||
kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks;
|
||||
constexpr GLint kFrontendMaxVaryingComponents = 64;
|
||||
constexpr GLint kFrontendMaxVaryingVectors = 8;
|
||||
// ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
|
||||
// gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
|
||||
// answer only; 32 is the GL 4.6 core minimum (table 23.57).
|
||||
constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
|
||||
constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
|
||||
// The compute stage's share of the combined sum below. Compute's own per-stage answer is
|
||||
// backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not
|
||||
// what that query returns - it is the GL 4.3 core minimum, present here only so the
|
||||
// combined total covers all SIX stages.
|
||||
constexpr GLint kFrontendMaxComputeUniformBlocksShare = 14;
|
||||
// GL 4.6 table 23.64 orders MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >=
|
||||
// every per-stage count, and the sum has to run over SIX stages, not three and not five.
|
||||
// Three (42) was the original bug. Five (70) replaced it and broke the middle term the
|
||||
// other way: compute's per-stage count is backend-derived and clamps at the binding count,
|
||||
// so a device reporting descriptor-indexing-scale uniform buffers (Adreno reports
|
||||
// maxPerStageDescriptorUniformBuffers = 16777216) advertised 84 compute blocks against a
|
||||
// combined 70. Six stages x 14 = 84, which is also exactly the binding-point count and the
|
||||
// arithmetic the GL 4.5 minimum of 84 bindings is built from, so the ordering is now tight
|
||||
// rather than accidental.
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks =
|
||||
kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
|
||||
kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks + kFrontendMaxComputeUniformBlocksShare;
|
||||
constexpr GLint kFrontendMaxVaryingComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVaryingVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
|
||||
constexpr GLint kFrontendMaxProgramTexelOffset = 7;
|
||||
constexpr GLint kFrontendMinProgramTexelOffset = -8;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
|
||||
// ARB_transform_feedback3's vertex-stream count. One is what this implementation can
|
||||
// actually emit to; see the GL_MAX_VERTEX_STREAMS case for why it is not four.
|
||||
constexpr GLint kFrontendMaxVertexStreams = 1;
|
||||
constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
|
||||
constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 36;
|
||||
// GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
|
||||
// how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
|
||||
// GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
|
||||
// the ceiling here.
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 84;
|
||||
constexpr GLint kFrontendSubpixelBits = 4;
|
||||
constexpr GLint kFrontendMaxSamples = 4;
|
||||
constexpr GLint kFrontendMaxSamples =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
|
||||
// ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
|
||||
// glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
|
||||
// plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
|
||||
// suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
|
||||
// caller reading its own uninitialised stack slot and strands an error for the next
|
||||
// unrelated call to trip over. The extension is deliberately NOT advertised, so the
|
||||
// numbers are a table entry, not a capability claim.
|
||||
constexpr GLint kFrontendMaxSubroutines = 256;
|
||||
constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;
|
||||
|
||||
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
||||
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
||||
@@ -133,9 +184,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.64: components + blocks * (blockSize / 4). The product has to be
|
||||
// formed in 64 bits and saturated on the way out - it overflowed a signed 32-bit int on
|
||||
// every Vulkan host that reports a large maxUniformBufferRange. A Mali driver answering
|
||||
// 0xFFFFFFFF saturates to INT32_MAX in the loader, and 14 * (2147483647 / 4) + 4096 wraps
|
||||
// to -1073737742, which the conformance suite read back as a limit "smaller than 58368".
|
||||
// Saturating instead of wrapping is also the only honest answer: an implementation that
|
||||
// can serve more components than a GLint holds still has to report a GLint.
|
||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||
GLint maxUniformBlockSizeBytes) {
|
||||
return maxDefaultUniformComponents + maxUniformBlocks * (maxUniformBlockSizeBytes / 4);
|
||||
const Int64 blocks = std::max<Int64>(static_cast<Int64>(maxUniformBlocks), 0);
|
||||
const Int64 componentsPerBlock = std::max<Int64>(static_cast<Int64>(maxUniformBlockSizeBytes), 0) / 4;
|
||||
const Int64 total = static_cast<Int64>(maxDefaultUniformComponents) + blocks * componentsPerBlock;
|
||||
return static_cast<GLint>(std::min<Int64>(total, std::numeric_limits<GLint>::max()));
|
||||
}
|
||||
|
||||
bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) {
|
||||
@@ -303,24 +364,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -472,10 +515,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
||||
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
||||
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
||||
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
||||
// just told the application to use. The backends clamp the realised count instead.
|
||||
// before it is advertised. gl_MaxSamples expands from the same floored number
|
||||
// (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
|
||||
//
|
||||
// THE FLOOR STOPS HERE, and that is the point. It used to be applied to
|
||||
// GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
|
||||
// on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
|
||||
// every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
|
||||
// reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
|
||||
// single sample, so flooring the query at 4 did not make four samples exist - it made the
|
||||
// backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
|
||||
// per-sample data it could never read back. Reporting what was probed turns that into an
|
||||
// honest "unsupported" the application can branch on.
|
||||
GLint GetAdvertisedMaxSamples() {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return kFrontendMaxSamples;
|
||||
@@ -483,6 +534,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
|
||||
// see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
|
||||
// floored rather than trusted.
|
||||
namespace {
|
||||
GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetAdvertisedColorTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedDepthTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedIntegerMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
|
||||
}
|
||||
|
||||
// Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
|
||||
// gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
const GLubyte* GetString(GLenum name) {
|
||||
static String vendorString;
|
||||
@@ -679,12 +774,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
// Same reason as the three above: the integer fallback would round the fraction to 0
|
||||
// or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE.
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
// Float-native state, so GL 4.6 core 2.2.2's "zero becomes FALSE, every other value
|
||||
// becomes TRUE" has to be applied to the VALUE. Answering these through the integer getter
|
||||
// below instead - which rounds - reported GL_FALSE for a perfectly non-zero level of 0.25,
|
||||
// and every other float state in this function already reads through GetFloatv for exactly
|
||||
// that reason.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const GLsizei componentCount = pname == GL_PATCH_DEFAULT_OUTER_LEVEL ? 4 : 2;
|
||||
GLfloat levels[4] = {};
|
||||
GetFloatv(pname, levels);
|
||||
for (GLsizei i = 0; i < componentCount; ++i) {
|
||||
params[i] = levels[i] != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -734,6 +847,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = depthRange.y();
|
||||
return;
|
||||
}
|
||||
// glPatchParameterfv's two states. Float-native, so they are answered here rather than
|
||||
// through the integer fallback below - which rounds, and would report 0 for a level of 0.5.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
params[0] = outer.x();
|
||||
params[1] = outer.y();
|
||||
params[2] = outer.z();
|
||||
params[3] = outer.w();
|
||||
return;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
params[0] = inner.x();
|
||||
params[1] = inner.y();
|
||||
return;
|
||||
}
|
||||
case GL_VIEWPORT_BOUNDS_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.ViewportBoundsRangeMin;
|
||||
@@ -799,6 +928,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POLYGON_OFFSET_UNITS:
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float-native state, so it is answered here rather than through the integer
|
||||
// fallback: glPolygonOffsetClamp(1, 1, 0.5) must read back as 0.5, not as 0.
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetClamp();
|
||||
return;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.SmoothLineWidthRangeMin;
|
||||
@@ -814,6 +948,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_COVERAGE_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetSampleCoverageValue();
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// Float state, so it has to be answered here rather than through the integer
|
||||
// fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0.
|
||||
params[0] = MG_State::pGLContext->GetMinSampleShadingValue();
|
||||
return;
|
||||
case GL_POINT_FADE_THRESHOLD_SIZE:
|
||||
// Float state: read it directly so the fractional part is not lost to the integer path.
|
||||
params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
|
||||
@@ -1035,6 +1174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetMinComputeWorkGroupSize(index);
|
||||
GLint backendValue = 0;
|
||||
if (getIntegeri) {
|
||||
MGP_FILL(GetIntegeri_v);
|
||||
getIntegeri(target, index, &backendValue);
|
||||
}
|
||||
*data = std::max(backendValue, minimum);
|
||||
@@ -1185,6 +1325,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
// The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
|
||||
// not left to the 32-bit fallback below: the conformance suite reads it with
|
||||
// glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
|
||||
// spec requires 2^32-1.
|
||||
params[0] = 0xFFFFFFFFLL;
|
||||
return;
|
||||
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
params[0] = static_cast<GLint64>(
|
||||
@@ -1208,6 +1355,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
@@ -1221,12 +1369,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint ints[4] = {};
|
||||
GetIntegerv(pname, ints);
|
||||
|
||||
// GL 4.6 core 22.1 gives glGetInteger64v the same accepted-pname set as glGetIntegerv, so
|
||||
// every pname the integer getter answers with several components owes them all here too.
|
||||
// A pname that reaches the `default:` arm writes params[0] and leaves the caller's other
|
||||
// components holding whatever they held, with no error to say so.
|
||||
switch (pname) {
|
||||
case GL_BLEND_COLOR:
|
||||
case GL_COLOR_CLEAR_VALUE:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_VIEWPORT:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
params[i] = static_cast<GLint64>(ints[i]);
|
||||
}
|
||||
@@ -1236,6 +1389,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
params[0] = static_cast<GLint64>(ints[0]);
|
||||
params[1] = static_cast<GLint64>(ints[1]);
|
||||
return;
|
||||
@@ -1267,6 +1421,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
count = 2;
|
||||
break;
|
||||
case GL_BLEND_COLOR:
|
||||
@@ -1274,6 +1429,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
count = 4;
|
||||
break;
|
||||
default:
|
||||
@@ -1313,6 +1469,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
// GL_TEXTURE_BUFFER_BINDING and GL_TEXTURE_BUFFER are the same token (0x8C2A): as a
|
||||
// glGetIntegerv pname it asks which BUFFER object is bound to the buffer-texture target,
|
||||
// not which texture is (that one is GL_TEXTURE_BINDING_BUFFER, handled by the texture-unit
|
||||
// decoder above).
|
||||
case GL_TEXTURE_BUFFER_BINDING: {
|
||||
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Texture).GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_BLEND:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
@@ -1368,6 +1533,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// this single case serves every getter flavor.
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
|
||||
return;
|
||||
// glClipControl's two state variables (GL 4.5 core table 23.7). They answer from the
|
||||
// state the entry point records, which is what the conformance suite's initial-value and
|
||||
// set-then-get cases read - the RASTERIZATION half of clip control is a separate,
|
||||
// backend-side question and does not gate the query.
|
||||
case GL_CLIP_ORIGIN:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipOrigin());
|
||||
return;
|
||||
case GL_CLIP_DEPTH_MODE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipDepthMode());
|
||||
return;
|
||||
case GL_COLOR_CLEAR_VALUE: {
|
||||
const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
|
||||
params[0] = static_cast<GLint>(clearColor.x());
|
||||
@@ -1427,19 +1602,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: 0;
|
||||
return;
|
||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||
// KHR_debug floors this at 64. It must agree with what GL_Debug.cpp actually enforces,
|
||||
// or an application that nests to the reported limit would take a STACK_OVERFLOW.
|
||||
*params = kFrontendMaxDebugGroupStackDepth;
|
||||
return;
|
||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||
*params = 1024; // agrees with GL_Debug.cpp's kMaxDebugMessageLength
|
||||
return;
|
||||
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||
// Size of the message log ring; KHR_debug requires at least 1.
|
||||
*params = kFrontendMaxDebugLoggedMessages;
|
||||
return;
|
||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||
*params = 0; // debug-group entrypoints are stubbed
|
||||
// The live depth, which is never 0: GL 4.6 core 20.6 creates the context with one
|
||||
// group already on the stack, and that is the one glPopDebugGroup may not pop.
|
||||
*params = GetDebugGroupStackDepth();
|
||||
return;
|
||||
case GL_CONTEXT_FLAGS: {
|
||||
*params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
|
||||
@@ -1654,6 +1831,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryUniformComponents;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
|
||||
*params = kFrontendMaxGeometryShaderInvocations;
|
||||
return;
|
||||
case GL_MAX_IMAGE_SAMPLES:
|
||||
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
|
||||
return;
|
||||
@@ -1707,6 +1887,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params =
|
||||
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
||||
return;
|
||||
// The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
|
||||
// constant that BuildTBuiltInResource expands, and the two must report the same number
|
||||
// (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
|
||||
// the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
|
||||
// They were the whole per-stage tess family: the table had been filled in only where the
|
||||
// honest answer was zero (the atomic counters, the image uniforms) or where a driver
|
||||
// query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
|
||||
// answer is a real resource count fell through to GL_INVALID_ENUM.
|
||||
case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_PATCH_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
|
||||
return;
|
||||
// Routed through the same clamp as every other per-stage block count so the
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
|
||||
// GL 4.6 table 23.64 cannot be broken by the two families moving independently.
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_SUBROUTINES:
|
||||
*params = kFrontendMaxSubroutines;
|
||||
return;
|
||||
case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
|
||||
*params = kFrontendMaxSubroutineUniformLocations;
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
return;
|
||||
@@ -1752,8 +1985,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_NUM_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_spirv_extensions / GL 4.6 core 22.2. An implementation that advertises no
|
||||
// SPIR-V extension answers zero here, and glGetStringi(GL_SPIR_V_EXTENSIONS, i) is then
|
||||
// never legally called - MobileGL runs the module through its own translation pipeline
|
||||
// and relies on no SPIR-V extension to do it, so zero is the true answer rather than a
|
||||
// placeholder.
|
||||
case GL_NUM_SPIR_V_EXTENSIONS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
|
||||
// agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
|
||||
case GL_NUM_SHADER_BINARY_FORMATS:
|
||||
*params = 0; // ShaderBinary entrypoints are stubbed
|
||||
*params = 1;
|
||||
return;
|
||||
case GL_SHADER_BINARY_FORMATS:
|
||||
*params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
|
||||
return;
|
||||
case GL_PACK_ALIGNMENT:
|
||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
|
||||
@@ -1812,6 +2058,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PRIMITIVE_RESTART_INDEX:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 4.6
|
||||
// core 22.1's float-to-integer rule.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPolygonOffsetClamp()));
|
||||
return;
|
||||
case GL_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0; // program-binary entrypoints are stubbed
|
||||
return;
|
||||
@@ -1897,6 +2148,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_MASK:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_SAMPLE_SHADING:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
|
||||
return;
|
||||
case GL_SAMPLE_MASK_VALUE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||
return;
|
||||
@@ -2008,6 +2266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
@@ -2115,7 +2374,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
*params = 1024 * 1024; // TODO
|
||||
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||
// state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
|
||||
// uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
|
||||
// indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
|
||||
// that no draw path ever consulted.
|
||||
*params = INT32_MAX;
|
||||
return;
|
||||
case GL_CONTEXT_PROFILE_MASK:
|
||||
// Reports the requested context profile (EGL defaults 3.x contexts to core);
|
||||
@@ -2171,8 +2435,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
break;
|
||||
case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS:
|
||||
// The CLAMPED block count, i.e. exactly what GL_MAX_COMPUTE_UNIFORM_BLOCKS answers.
|
||||
// GL 4.6 table 23.64 defines this as the components reachable through the blocks a
|
||||
// stage may declare, so deriving it from the raw backend number described 256 blocks
|
||||
// an application is only ever allowed 84 of.
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents,
|
||||
dynamicParameters.MaxComputeUniformBlocks,
|
||||
ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
|
||||
@@ -2216,16 +2484,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
||||
break;
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedColorTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||
kFrontendMaxFragmentUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
|
||||
kFrontendMaxGeometryUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
|
||||
@@ -2239,14 +2507,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
|
||||
kFrontendMaxVertexUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||
break;
|
||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedDepthTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||
*params = dynamicParameters.MaxFramebufferWidth;
|
||||
@@ -2273,7 +2541,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||
break;
|
||||
case GL_MAX_INTEGER_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedIntegerMaxSamples();
|
||||
break;
|
||||
case GL_MAX_RENDERBUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxRenderbufferSize;
|
||||
@@ -2284,12 +2552,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PATCH_VERTICES:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
|
||||
break;
|
||||
// Float state, so glGetIntegerv rounds it (GL 4.6 core 2.2.2) - the exact values come back
|
||||
// through glGetFloatv. Answered here so glGetBooleanv, which delegates to this getter for
|
||||
// everything its own switch does not handle, does not report INVALID_ENUM for them.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
for (Uint i = 0; i < 4; ++i) params[i] = static_cast<GLint>(std::lround(outer[i]));
|
||||
break;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
|
||||
// GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
|
||||
// the DirectVulkan draw path relies on this staying false (it resolves primitive restart
|
||||
// to "never" for a PATCH_LIST topology on the strength of it).
|
||||
case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
|
||||
*params = GL_FALSE;
|
||||
break;
|
||||
case GL_MAX_PATCH_VERTICES:
|
||||
*params = dynamicParameters.MaxPatchVertices;
|
||||
break;
|
||||
case GL_MAX_TESS_GEN_LEVEL:
|
||||
*params = dynamicParameters.MaxTessGenLevel;
|
||||
break;
|
||||
// Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
|
||||
// COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
|
||||
// conformance suite's own formula exactly, so the two cannot drift.
|
||||
case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
// ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
|
||||
// for a stronger reason: a cull distance discards the whole primitive, so advertising
|
||||
// eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
|
||||
// the honest answer on a host with no cull-distance route, and the conformance suite then
|
||||
// skips the functional cases instead of failing them deep inside a pixel comparison.
|
||||
case GL_MAX_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCullDistances;
|
||||
break;
|
||||
case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCombinedClipAndCullDistances;
|
||||
break;
|
||||
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||
*params = dynamicParameters.MinProgramTextureGatherOffset;
|
||||
break;
|
||||
@@ -2340,7 +2652,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
// ONE, which is under the GL 4.5 core table 23.62 minimum of four and is a known,
|
||||
// deliberate non-conformance. It was briefly raised to 4 on the theory that streams
|
||||
// 1..3 could exist and be permanently empty; measuring that decision refuted it.
|
||||
// Raising the limit un-gates two CTS cases per package across KHR-GL40..GL46 -
|
||||
// transform_feedback.draw_xfb_stream_test (which stops being skipped) and
|
||||
// transform_feedback3.multiple_streams (which stops reporting NotSupported) - and
|
||||
// both then fail, because nothing in the shader pipeline supports layout(stream = N),
|
||||
// EmitStreamVertex or EndStreamPrimitive, and because the query state machine tracks
|
||||
// one active query per TARGET rather than per (target, stream). That is 14 new
|
||||
// failures against 2 gained limits passes, and a 4 nothing can back is the
|
||||
// advertised-caps lie with the sign flipped.
|
||||
//
|
||||
// The real fix is the feature, not the number: per-stream capture needs
|
||||
// layout(stream = N) through the transpiler plus per-(target, stream) query slots,
|
||||
// which DirectVulkan could back with VK_EXT_transform_feedback's geometryStreams and
|
||||
// DirectGLES cannot back at all (ES has no vertex streams). Until that lands, one is
|
||||
// the honest count and every stream-addressing entry point bounds itself by THIS
|
||||
// query, so raising it later moves them all together.
|
||||
*params = kFrontendMaxVertexStreams;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
@@ -2357,15 +2687,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxTextureSize;
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
|
||||
// Never advertise more bindings than the state layer's indexed-binding array can track
|
||||
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
|
||||
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
|
||||
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
|
||||
// (36), so the clamp never under-advertises.
|
||||
// advertised index and expects no error. The floor is the GL 4.5 core minimum, and
|
||||
// the array was widened to exactly it, so the two coincide by construction.
|
||||
//
|
||||
// WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
|
||||
// family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
|
||||
// not a driver binding point. Neither backend forwards it as one on the draw path:
|
||||
// DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
|
||||
// (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
|
||||
// resolves each block to a descriptor. So what the host driver's count bounds is how
|
||||
// many blocks ONE PROGRAM may use, not how many points an application may bind.
|
||||
//
|
||||
// That per-program number is NOT GL_MAX_COMBINED_UNIFORM_BLOCKS (84, the six-stage
|
||||
// sum): no single program can reach it. A graphics program is bounded by the five
|
||||
// graphics stages' per-stage counts, 14 each, so 70 blocks plus the global UBO at ES
|
||||
// point 0 = 71 - inside the ES 3.2 minimum of 72. A compute program is bounded by
|
||||
// GL_MAX_COMPUTE_UNIFORM_BLOCKS, which on DirectGLES is the ES driver's own count
|
||||
// (GL-scale, ~14) and on DirectVulkan is served from descriptors with no ES binding
|
||||
// points involved. Raising any per-stage graphics count past 14 is what would break
|
||||
// this, so that is the edit to check against the ES ceiling - not this one.
|
||||
static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
|
||||
kFrontendMinUniformBufferBindings,
|
||||
"the indexed-binding array must be able to hold every advertised uniform binding point");
|
||||
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
|
||||
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
|
||||
break;
|
||||
}
|
||||
case GL_MAX_UNIFORM_BLOCK_SIZE:
|
||||
*params = dynamicParameters.MaxUniformBlockSize;
|
||||
break;
|
||||
@@ -2403,7 +2754,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
|
||||
break;
|
||||
case GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<GLint>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
// The STORAGE alignment, which is its own limit - this used to answer with the
|
||||
// uniform one. They differ on real hardware (Adreno 830: 32 uniform, 64 storage), and
|
||||
// under-reporting it is silent: ValidateBindBufferRange accepts the offset, the ES
|
||||
// driver accepts it too without raising an error, and the shader's writes then land
|
||||
// at an address the application never bound.
|
||||
*params = static_cast<GLint>(dynamicParameters.ShaderStorageBufferOffsetAlignment);
|
||||
break;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
params[0] = static_cast<GLint>(dynamicParameters.SmoothLineWidthRangeMin);
|
||||
|
||||
@@ -25,7 +25,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
|
||||
// core minimum. Frontend multisample validators have to honour this ceiling for every
|
||||
// format, otherwise MobileGL rejects a sample count it advertised itself.
|
||||
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
|
||||
// ceilings below have a minimum of one and are reported as probed.
|
||||
GLint GetAdvertisedMaxSamples();
|
||||
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
|
||||
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
|
||||
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
|
||||
// the query promised - it used to floor both at 4 and then let the backend quietly
|
||||
// under-allocate whatever the driver could not actually provide.
|
||||
GLint GetAdvertisedColorTextureMaxSamples();
|
||||
GLint GetAdvertisedDepthTextureMaxSamples();
|
||||
GLint GetAdvertisedIntegerMaxSamples();
|
||||
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
|
||||
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
|
||||
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
|
||||
//
|
||||
// Shared rather than duplicated because two callers need the identical number and disagreeing
|
||||
// would be a silent bug: the query itself, and the draw path's write of the reserved
|
||||
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
|
||||
// is exactly what the sample_variables CTS does.
|
||||
GLint ResolveDrawFramebufferSampleCount();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "Config.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -19,6 +21,7 @@
|
||||
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
||||
@@ -30,10 +33,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckShaderNameValidity(Uint shader) {
|
||||
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) {
|
||||
// The mirror of CheckProgramNameValidity below, and for the same reason: programs and
|
||||
// shaders are drawn from ONE name space (ProgramState hands both out of a single
|
||||
// generator), so a name that exists but belongs to a PROGRAM is the wrong kind of
|
||||
// object - GL 3.3 core 2.11.x makes that INVALID_OPERATION - while a name GL never
|
||||
// handed out is INVALID_VALUE. This half of the split was missing, so every shader
|
||||
// entry point handed a program name reported INVALID_VALUE; the conformance suite
|
||||
// reads exactly that code back from glSpecializeShader.
|
||||
const ErrorCode error = (shader != 0 && MG_State::pGLContext->ValidateProgramName(shader))
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shader) + " is not a valid name."));
|
||||
std::to_string(shader) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a shader object."
|
||||
: " is not a valid name.")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -245,6 +260,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6.3: INVALID_VALUE when uniformBlockBinding >= MAX_UNIFORM_BUFFER_BINDINGS.
|
||||
// The storage-block twin below has always had this check; the uniform one never did, and the
|
||||
// value it stores is used as a RAW SUBSCRIPT into the state layer's fixed indexed-binding
|
||||
// array on every draw and dispatch (DirectGLES's per-program UBO rebind, DirectVulkan's
|
||||
// descriptor resolve, whose only guard is a MOBILEGL_ASSERT that compiles away in release).
|
||||
// An out-of-range binding therefore did not merely go unreported - it read past the array and
|
||||
// dereferenced whatever SharedPtr it found there.
|
||||
bool ValidateUniformBlockBinding(GLuint binding) {
|
||||
// Exactly what glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS) advertises: the state
|
||||
// layer's array width, which the getter clamps to as well.
|
||||
const SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform);
|
||||
if (binding < maxBindingCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("Uniform block binding {} is not less than GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).", binding,
|
||||
maxBindingCount)));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateShaderStorageBlockBinding(GLuint binding) {
|
||||
SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
@@ -307,9 +346,195 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompileShader_State(GLuint shader) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
// ARB_gl_spirv: "INVALID_OPERATION is generated by CompileShader if shader has been
|
||||
// associated with a SPIR-V binary". Such an object has no GLSL source to compile - it is
|
||||
// waiting for glSpecializeShader, which is the operation that compiles it.
|
||||
if (shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" holds a SPIR-V binary; use glSpecializeShader instead of glCompileShader."));
|
||||
return;
|
||||
}
|
||||
shaderObject->Compile();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// GL_ARB_gl_spirv
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
void ShaderBinary_State(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary,
|
||||
GLsizei length) {
|
||||
if (count < 0 || length < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count and length must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL_NUM_SHADER_BINARY_FORMATS advertises exactly one format, so every other value is
|
||||
// INVALID_ENUM (GL 4.6 core 7.2). This is the check that used to be missing entirely -
|
||||
// the entry point was a silent stub, so an application handed a format nothing supports
|
||||
// and was told nothing.
|
||||
if (binaryformat != GL_SHADER_BINARY_FORMAT_SPIR_V) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"binaryformat must be GL_SHADER_BINARY_FORMAT_SPIR_V."));
|
||||
return;
|
||||
}
|
||||
if (count == 0) return;
|
||||
if (shaders == nullptr || (length > 0 && binary == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "shaders and binary must not be null."));
|
||||
return;
|
||||
}
|
||||
// A SPIR-V module is a sequence of 32-bit words, so a length that is not a multiple of
|
||||
// four cannot be one (ARB_gl_spirv makes this INVALID_VALUE).
|
||||
if ((length % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"length must be a multiple of four for a SPIR-V module."));
|
||||
return;
|
||||
}
|
||||
|
||||
// EVERY name is validated before ANY of them is written: the entry point is all-or-
|
||||
// nothing, and half-applying it would leave some objects holding a module the call was
|
||||
// rejected for. The duplicate check is the extension's own ("INVALID_VALUE ... if the
|
||||
// same shader object is specified more than once").
|
||||
std::set<GLuint> seen;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!seen.insert(shaders[i]).second) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shaders[i]) +
|
||||
" appears more than once in `shaders`."));
|
||||
return;
|
||||
}
|
||||
if (!MG_State::pGLContext->ValidateShaderName(shaders[i])) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shaders[i]) + " is not the name of a shader object."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT wordCount = static_cast<SizeT>(length) / 4;
|
||||
Vector<Uint32> module(wordCount);
|
||||
if (wordCount != 0) {
|
||||
Memcpy(module.data(), binary, static_cast<SizeT>(length));
|
||||
}
|
||||
// spirv-val here, not at glSpecializeShader: this is where the words arrive, and past it
|
||||
// they reach SPIRV-Cross, which parses rather than validates. ARB_gl_spirv lets an
|
||||
// implementation reject an invalid module at either call; rejecting at the earlier one
|
||||
// means the application's error is reported next to the data that caused it.
|
||||
if (const auto validated = MG_Util::ShaderTranspiler::ShaderCompiler::ValidateSpirvModule(module);
|
||||
!validated) {
|
||||
MGLOG_D("%s: rejected SPIR-V module: %s", __func__, validated.error().log.c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, validated.error().log));
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
auto& shaderObject = TryToGetShaderObject(shaders[i]);
|
||||
if (!shaderObject) continue;
|
||||
// A copy per object, not a shared buffer: each shader object may be specialized with
|
||||
// different constants, and each specialization re-reads its own original words.
|
||||
Vector<Uint32> perObject = module;
|
||||
shaderObject->SetSpirvBinary(Move(perObject));
|
||||
}
|
||||
}
|
||||
|
||||
void SpecializeShader_State(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
if (!shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has no SPIR-V binary; call glShaderBinary first."));
|
||||
return;
|
||||
}
|
||||
// ARB_gl_spirv: a shader that has already been specialized may not be specialized again
|
||||
// until glShaderBinary re-associates a module with it.
|
||||
if (shaderObject->HasBeenSpecialized()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has already been specialized; re-associate its module with "
|
||||
"glShaderBinary before specializing it again."));
|
||||
return;
|
||||
}
|
||||
// pEntryPoint names the entry point to specialize; there is no default. A null pointer
|
||||
// cannot name one, and neither can the empty string.
|
||||
if (pEntryPoint == nullptr || *pEntryPoint == '\0') {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pEntryPoint must name an entry point."));
|
||||
return;
|
||||
}
|
||||
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pConstantIndex and pConstantValue must not be null."));
|
||||
return;
|
||||
}
|
||||
// "INVALID_VALUE is generated if any value in pConstantIndex is repeated" - checked before
|
||||
// anything is applied, for the same all-or-nothing reason glShaderBinary checks its names
|
||||
// up front.
|
||||
Vector<Uint32> constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants);
|
||||
Vector<Uint32> constantValues(pConstantValue, pConstantValue + numSpecializationConstants);
|
||||
{
|
||||
std::set<Uint32> seen;
|
||||
for (const Uint32 id : constantIds) {
|
||||
if (seen.insert(id).second) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"constant index " + std::to_string(id) + " is repeated."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const String entryPoint(pEntryPoint);
|
||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
|
||||
using SpecializationFailure = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializationFailure;
|
||||
SpecializationFailure failure = SpecializationFailure::None;
|
||||
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
|
||||
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
|
||||
if (!specialized) {
|
||||
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
|
||||
specialized.error().log.c_str());
|
||||
// The two conditions ARB_gl_spirv ENUMERATES are GL errors, and an erroring GL command
|
||||
// must have no other effect - so the shader object is left exactly as it was rather
|
||||
// than being pushed into a failed-compile state. Anything else is a genuine compile
|
||||
// failure of a well-formed request, which the extension routes through COMPILE_STATUS
|
||||
// and the info log exactly as glCompileShader does.
|
||||
if (failure == SpecializationFailure::UnknownEntryPoint ||
|
||||
failure == SpecializationFailure::UnknownConstantId) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
|
||||
specialized.value().xfbBufferMode);
|
||||
}
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
|
||||
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
|
||||
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
|
||||
@@ -744,12 +969,77 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_PROGRAM_SEPARABLE:
|
||||
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
|
||||
// The LATCHED flag, not the live one: glProgramParameteri's write takes effect at the
|
||||
// next link (GL 4.6 core 7.3), so a program told to be separable and then never
|
||||
// linked still reports GL_FALSE.
|
||||
*params = programObject->GetLinkedSeparable() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
|
||||
// The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as
|
||||
// GL_COMPUTE_WORK_GROUP_SIZE above, and for the same reason: "a linked program object
|
||||
// with a geometry shader" is one whose EXECUTABLE has the stage, so an
|
||||
// attached-but-not-yet-linked shader must give INVALID_OPERATION rather than the previous
|
||||
// link's value. The geometry three used to be listed here only to fall through into the
|
||||
// INVALID_ENUM default, and the tessellation five were not listed at all.
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a geometry shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_GEOMETRY_VERTICES_OUT: *params = programObject->GetGeometryVerticesOut(); break;
|
||||
case GL_GEOMETRY_INPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryInputType()); break;
|
||||
case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryOutputType()); break;
|
||||
default: *params = programObject->GetGeometryShaderInvocations(); break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_CONTROL_OUTPUT_VERTICES: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessControl)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation control shader."));
|
||||
return;
|
||||
}
|
||||
*params = programObject->GetTessControlOutputVertices();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_GEN_MODE:
|
||||
case GL_TESS_GEN_SPACING:
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
case GL_TESS_GEN_POINT_MODE: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessEval)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation evaluation shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_TESS_GEN_MODE: *params = static_cast<GLint>(programObject->GetTessGenMode()); break;
|
||||
case GL_TESS_GEN_SPACING: *params = static_cast<GLint>(programObject->GetTessGenSpacing()); break;
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
*params = static_cast<GLint>(programObject->GetTessGenVertexOrder());
|
||||
break;
|
||||
default: *params = programObject->GetTessGenPointMode() ? GL_TRUE : GL_FALSE; break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -811,8 +1101,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||
break;
|
||||
case GL_SHADER_SOURCE_LENGTH:
|
||||
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
|
||||
case GL_SHADER_SOURCE_LENGTH: {
|
||||
// The APPLICATION's source, which is empty for a shader that came from glShaderBinary -
|
||||
// see ShaderObject::GetApplicationShaderSource.
|
||||
const auto& source = shaderObject->GetApplicationShaderSource();
|
||||
*params = source.empty() ? 0 : (GLint)source.length() + 1;
|
||||
break;
|
||||
}
|
||||
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
|
||||
// while the object stands for an application-supplied module. It is the FIRST thing the
|
||||
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
|
||||
// default arm below and take the whole test with it.
|
||||
case GL_SPIR_V_BINARY:
|
||||
*params = shaderObject->HasSpirvBinary() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
|
||||
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
|
||||
@@ -858,13 +1159,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
|
||||
auto& src = shaderObject->GetShaderSource();
|
||||
auto& src = shaderObject->GetApplicationShaderSource();
|
||||
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
|
||||
}
|
||||
|
||||
GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return -1;
|
||||
// GL 4.6 core 7.6: "INVALID_OPERATION is generated if program has not been successfully
|
||||
// linked". Answering -1 silently is not the same thing - the conformance suite reads the
|
||||
// error, not the location.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return -1;
|
||||
}
|
||||
auto loc = programObject->GetUniformLocation(name);
|
||||
MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
|
||||
return loc;
|
||||
@@ -1277,11 +1588,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
template <GLsizei ItemCount, typename T>
|
||||
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
// The link check comes BEFORE the location == -1 early-out, not after. GL 4.6 core 7.6
|
||||
// makes an unlinked program INVALID_OPERATION regardless of the location, and -1 is
|
||||
// exactly the location an application holds after glGetUniformLocation on such a program -
|
||||
// so checking -1 first swallowed the very case the rule exists for.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1289,6 +1602,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
// "If location is equal to -1, the data passed in will be silently ignored and the
|
||||
// specified uniform variable will not be changed" - after the program itself has been
|
||||
// found acceptable.
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
@@ -1699,8 +2016,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1712,14 +2027,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1731,6 +2046,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1756,8 +2073,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1769,6 +2084,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1790,8 +2107,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
|
||||
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1803,6 +2118,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
@@ -1836,6 +2153,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Program object" + std::to_string(program) + " that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateUniformBlockBinding(uniformBlockBinding)) return;
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -2083,6 +2401,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BindAttribLocation_State(program, index, name);
|
||||
}
|
||||
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) {
|
||||
ShaderBinary_State(count, shaders, binaryformat, binary, length);
|
||||
}
|
||||
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
SpecializeShader_State(shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue);
|
||||
}
|
||||
|
||||
void CompileShader(GLuint shader) {
|
||||
CompileShader_State(shader);
|
||||
}
|
||||
@@ -2342,7 +2669,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2352,6 +2678,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
|
||||
}
|
||||
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2368,7 +2695,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2378,6 +2704,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
|
||||
}
|
||||
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2394,7 +2721,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2404,6 +2730,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
|
||||
}
|
||||
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2420,7 +2747,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2430,6 +2756,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
|
||||
}
|
||||
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2446,7 +2773,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2456,6 +2782,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
|
||||
}
|
||||
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2472,7 +2799,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2482,6 +2808,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
|
||||
}
|
||||
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2498,7 +2825,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2508,6 +2834,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
|
||||
}
|
||||
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2524,7 +2851,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2534,6 +2860,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
|
||||
}
|
||||
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2550,7 +2877,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2560,6 +2886,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
|
||||
}
|
||||
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
|
||||
@@ -3072,6 +3399,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support shader storage block binding."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ShaderStorageBlockBinding);
|
||||
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void AttachShader(GLuint program, GLuint shader);
|
||||
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
|
||||
void CompileShader(GLuint shader);
|
||||
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
|
||||
// the module to one or more shader objects, glSpecializeShader names its entry point and
|
||||
// supplies its specialization constants and is what actually compiles them.
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue);
|
||||
GLuint CreateProgram(void);
|
||||
GLuint CreateShader(GLenum type);
|
||||
void DeleteProgram(GLuint program);
|
||||
|
||||
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Program {} has not been linked successfully.", program));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
|
||||
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
|
||||
// whose live flag was cleared after a separable link is still a legal stage, and a
|
||||
// program whose live flag was set after a non-separable link is not.
|
||||
if (!programObject->GetLinkedSeparable()) {
|
||||
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||
std::format("Program {} was not linked as a separable program.", program));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -59,6 +60,62 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
|
||||
// target (0 = none). A map rather than a field per target: the eleven behave identically
|
||||
// and none of them has any state beyond "which object is counting".
|
||||
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
|
||||
|
||||
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
|
||||
// ADVERTISED list rather than from a capability bit for the same reason
|
||||
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
|
||||
// support" and "the tessellation-conditional API surface is open" the same fact by
|
||||
// construction, so the day a backend starts advertising the string the surface below opens
|
||||
// with it and no second edit is owed.
|
||||
Bool AdvertisesTessellationShaderExtension() {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) return false;
|
||||
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
|
||||
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
|
||||
}
|
||||
|
||||
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
|
||||
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
|
||||
// by which an application could learn otherwise before calling. MobileGL instruments none of
|
||||
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
|
||||
// zero for these targets, which is the spec's own signal that the counter is unsupported and
|
||||
// its results indeterminate. That is an honest zero, not an advertised capability - the
|
||||
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
|
||||
// informative.
|
||||
//
|
||||
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
|
||||
// makes them conditional on tessellation support rather than unconditional, and the only
|
||||
// thing an application (or the conformance suite) can read to decide whether an
|
||||
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
|
||||
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
|
||||
// tessellation-conditional token while withholding the string that announces the condition
|
||||
// is self-contradictory, and it is the contradiction the suite catches
|
||||
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
|
||||
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
|
||||
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
|
||||
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
|
||||
switch (target) {
|
||||
case GL_VERTICES_SUBMITTED:
|
||||
case GL_PRIMITIVES_SUBMITTED:
|
||||
case GL_VERTEX_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
|
||||
case GL_FRAGMENT_SHADER_INVOCATIONS:
|
||||
case GL_COMPUTE_SHADER_INVOCATIONS:
|
||||
case GL_CLIPPING_INPUT_PRIMITIVES:
|
||||
case GL_CLIPPING_OUTPUT_PRIMITIVES:
|
||||
return true;
|
||||
case GL_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
return AdvertisesTessellationShaderExtension();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -108,6 +165,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ResetQueryObjectLocked(QueryObject* queryObject) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -122,6 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
|
||||
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
|
||||
if (endTimeElapsedQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndTimeElapsedQuery);
|
||||
endTimeElapsedQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -201,6 +260,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
Uint64 result = 0;
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (queryObject->backendHandle && getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
|
||||
// Not ready. The whole point of the no-wait form is that the caller's
|
||||
@@ -215,6 +275,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -230,6 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
|
||||
MGP_FILL(IsQueryResultAvailable);
|
||||
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
|
||||
return true;
|
||||
}
|
||||
@@ -241,6 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Uint64 result = 0;
|
||||
if (queryObject->backendHandle) {
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
|
||||
// The backend could not produce the result YET (e.g. a
|
||||
@@ -261,6 +324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -366,10 +430,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
|
||||
queryObject->active = false;
|
||||
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
@@ -382,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -410,7 +479,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
@@ -426,10 +497,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on this target.");
|
||||
@@ -448,10 +521,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
|
||||
// The object still becomes a real, target-latched query so every other rule about it
|
||||
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
|
||||
} else if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
MGP_FILL(BeginXfbPrimitivesQuery);
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||
@@ -460,9 +538,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
queryObject->geometryCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
||||
} else if (isOcclusionQuery) {
|
||||
MGP_FILL(BeginOcclusionQuery);
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
MGP_FILL(BeginTimeElapsedQuery);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
@@ -476,15 +556,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
return;
|
||||
@@ -494,9 +578,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// The result is a definite zero rather than an unread backend handle, so a later
|
||||
// GetQueryObject* answers immediately and never waits on something that was never
|
||||
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
|
||||
queryObject->cachedResult = 0;
|
||||
queryObject->resultCached = true;
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
MGP_FILL(EndXfbPrimitivesQuery);
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
}
|
||||
@@ -506,6 +602,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -522,6 +619,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -560,6 +658,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
|
||||
MGP_FILL(QueryCounterTimestamp);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||
queryObject->ended = true;
|
||||
@@ -657,7 +756,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
const auto it = g_activePipelineStatisticsQueryIds.find(target);
|
||||
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
|
||||
} else {
|
||||
*params = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -668,6 +772,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins.
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
|
||||
// results are indeterminate. The conformance suite reads exactly this and skips
|
||||
// the functional half of each such target, which is the outcome an uninstrumented
|
||||
// counter should produce.
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
@@ -676,6 +788,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
MGP_FILL(IsTimerQuerySupported);
|
||||
const Bool supported =
|
||||
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
|
||||
*params = supported ? 64 : 0;
|
||||
@@ -741,14 +854,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsPerVertexStreamQueryTarget(GLenum target) {
|
||||
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
}
|
||||
|
||||
// The indexed query entry points differ from the plain ones only in the vertex
|
||||
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
|
||||
// for the two transform feedback targets and zero for every other target. With a
|
||||
// single vertex stream both bounds are 1, so a valid call is always index 0 and
|
||||
// forwards to the unindexed implementation.
|
||||
// for the two transform feedback targets and zero for every other target. MobileGL
|
||||
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
|
||||
// which is what makes the three forwards below equivalent to the unindexed entry points.
|
||||
//
|
||||
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
|
||||
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
|
||||
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
|
||||
// per-TARGET globals and would end - or report - a query begun on a different stream.
|
||||
// Raising that limit therefore means giving each active query a stream index and
|
||||
// comparing it here, not just changing the number.
|
||||
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
|
||||
const Bool perStreamTarget =
|
||||
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
|
||||
GLint maxVertexStreams = 1;
|
||||
if (perStreamTarget) {
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
@@ -761,6 +884,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: "index must be zero for this query target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
|
||||
@@ -806,6 +930,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
|
||||
@@ -328,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
|
||||
}
|
||||
|
||||
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
|
||||
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
|
||||
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
|
||||
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
|
||||
void MinSampleShading_State(GLfloat value) {
|
||||
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void PolygonOffset_State(GLfloat factor, GLfloat units) {
|
||||
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
|
||||
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
|
||||
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
|
||||
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
|
||||
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
|
||||
static_cast<Float>(clamp));
|
||||
}
|
||||
|
||||
void ClipControl_State(GLenum origin, GLenum depth) {
|
||||
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
|
||||
// GL_INVALID_ENUM with the state left untouched.
|
||||
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
|
||||
MG_Util::ConvertGLEnumToString(origin) + "."));
|
||||
return;
|
||||
}
|
||||
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
|
||||
MG_Util::ConvertGLEnumToString(depth) + "."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->SetClipControl(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode_State(GLenum face, GLenum mode) {
|
||||
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
|
||||
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
|
||||
@@ -1013,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SampleCoverage_State(value, invert);
|
||||
}
|
||||
|
||||
void MinSampleShading(GLfloat value) {
|
||||
MinSampleShading_State(value);
|
||||
}
|
||||
|
||||
void PolygonOffset(GLfloat factor, GLfloat units) {
|
||||
PolygonOffset_State(factor, units);
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
PolygonOffsetClamp_State(factor, units, clamp);
|
||||
}
|
||||
|
||||
void ClipControl(GLenum origin, GLenum depth) {
|
||||
ClipControl_State(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode(GLenum face, GLenum mode) {
|
||||
PolygonMode_State(face, mode);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void StencilFunc(GLenum func, GLint ref, GLuint mask);
|
||||
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
void SampleCoverage(GLfloat value, GLboolean invert);
|
||||
void MinSampleShading(GLfloat value);
|
||||
void PolygonOffset(GLfloat factor, GLfloat units);
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
|
||||
void ClipControl(GLenum origin, GLenum depth);
|
||||
void PolygonMode(GLenum face, GLenum mode);
|
||||
void PointSize(GLfloat size);
|
||||
void PointParameterf(GLenum pname, GLfloat param);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return static_cast<Float>(*(const GLint*)param);
|
||||
}
|
||||
|
||||
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
|
||||
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
|
||||
// reads exactly one component and does not care.
|
||||
Bool IsVectorOnlySamplerPname(GLenum pname) {
|
||||
return pname == GL_TEXTURE_BORDER_COLOR;
|
||||
}
|
||||
|
||||
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
|
||||
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
|
||||
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
|
||||
// sampler path funnels all three spellings through one void* function, which is precisely how
|
||||
// it came to write a fixed type regardless of the caller.
|
||||
//
|
||||
// Truncation rather than rounding for the float -> integer direction, matching the texture
|
||||
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
|
||||
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
|
||||
// sampler queried the same way must answer the same number.
|
||||
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = value;
|
||||
return;
|
||||
}
|
||||
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
|
||||
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
|
||||
// conversion is the well-defined modular one, and it is what the texture-side
|
||||
// GetTexParameterIuiv fallback does.
|
||||
const GLint asInt = static_cast<GLint>(value);
|
||||
if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(asInt);
|
||||
} else {
|
||||
*(GLint*)params = asInt;
|
||||
}
|
||||
}
|
||||
|
||||
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = static_cast<GLfloat>(value);
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = value;
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(value);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
|
||||
if (param == nullptr) return false;
|
||||
|
||||
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
|
||||
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
|
||||
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
|
||||
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
|
||||
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
|
||||
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
|
||||
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
|
||||
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (param == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isFloat) {
|
||||
const auto* values = (const GLfloat*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else if (!isIntegerCommand) {
|
||||
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
|
||||
const auto* values = (const GLint*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto* values = (const GLuint*)param;
|
||||
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
|
||||
@@ -128,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (params == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
|
||||
|
||||
using namespace MG_Util;
|
||||
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
|
||||
// decides the destination type. Writing a fixed type regardless - which is what these case
|
||||
// labels used to do - hands back the other type's bit pattern rather than a converted value:
|
||||
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
|
||||
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
|
||||
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
|
||||
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
|
||||
// how the same bug class was already found and fixed once for a single pname.
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_WRAP_S:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_T:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_R:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
|
||||
samplerObj->GetMipmapMode()));
|
||||
break;
|
||||
case GL_TEXTURE_MAG_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
|
||||
SamplerMipmapMode::None));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMinLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMaxLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
|
||||
break;
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
*(GLfloat*)params = samplerObj->GetLodBias();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
|
||||
}
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_FUNC:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
if (isFloat) {
|
||||
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
out[1] = color.y();
|
||||
out[2] = color.z();
|
||||
out[3] = color.w();
|
||||
} else if (!isIntegerCommand) {
|
||||
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
|
||||
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
|
||||
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
|
||||
const auto& color = samplerObj->GetBorderColor();
|
||||
auto* out = (GLint*)params;
|
||||
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
|
||||
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
|
||||
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
|
||||
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto& color = samplerObj->GetBorderColorUI();
|
||||
auto* out = (GLuint*)params;
|
||||
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sampler == 0) {
|
||||
textureUnit.SetSamplerObject(nullptr);
|
||||
} else {
|
||||
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
|
||||
// name, which is why this cannot go through the shared SamplerImpl validator.
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
|
||||
// name, so the shared validator answers for all of them.
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
|
||||
if (!doesSamplerObjectCreated) {
|
||||
MG_State::pGLContext->CreateSamplerObject(sampler);
|
||||
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, true);
|
||||
SetSamplerParam_State(sampler, pname, param, false, true, true);
|
||||
}
|
||||
|
||||
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, true);
|
||||
}
|
||||
|
||||
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, true, false);
|
||||
SetSamplerParam_State(sampler, pname, param, true, false, false);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
|
||||
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
|
||||
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
|
||||
// past the caller's single stack scalar - taking the address of a by-value argument and handing
|
||||
// it to a four-component reader is what these used to do. The texture-side twins already answer
|
||||
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
|
||||
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameteriv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameterfv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, true);
|
||||
GetSamplerParam_State(sampler, pname, params, false, true, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, true, false);
|
||||
GetSamplerParam_State(sampler, pname, params, true, false, false);
|
||||
}
|
||||
|
||||
void GenSamplers(GLsizei count, GLuint* samplers) {
|
||||
|
||||
@@ -12,11 +12,17 @@
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
|
||||
// sampler object previously returned from a call to GenSamplers." That class is shared by every
|
||||
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
|
||||
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
|
||||
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
|
||||
Bool ValidateSamplerName(GLuint sampler) {
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GL_Sync.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -56,6 +57,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
@@ -69,14 +71,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
|
||||
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
|
||||
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
|
||||
// flush it never got.
|
||||
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
|
||||
// the enum alone left glGetError() clean and the failure indistinguishable from a
|
||||
// genuine wait failure on a live sync.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
MGP_FILL(ClientWaitSync);
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
@@ -95,10 +115,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
return;
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
MGP_FILL(WaitSync);
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -119,14 +143,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
||||
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
|
||||
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
|
||||
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
|
||||
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
|
||||
// but a mistyped pname answered a bare 0 that no query ever returns.
|
||||
if (bufSize < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
|
||||
return;
|
||||
}
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
@@ -140,6 +179,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
MGP_FILL(GetSyncStatus);
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
@@ -152,7 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
value = static_cast<GLint>(syncObject->flags);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
|
||||
"GL_SYNC_FLAGS."));
|
||||
if (length) {
|
||||
*length = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (length) {
|
||||
@@ -185,6 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,24 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
|
||||
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
|
||||
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
|
||||
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
|
||||
//
|
||||
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
|
||||
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
|
||||
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
|
||||
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
|
||||
// that adds row padding / a row-length override / a skip offset, is rejected with
|
||||
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
|
||||
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
|
||||
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
|
||||
// clears take the same list, so it is shared rather than written out twice.
|
||||
Bool IsBufferTextureInternalFormat(GLenum internalformat);
|
||||
@@ -37,8 +52,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum format, GLenum type, const void* pixels);
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
||||
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
GLsizei imageSize, const void* data);
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
|
||||
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
||||
const void* data);
|
||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||
@@ -60,6 +80,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
|
||||
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
|
||||
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
|
||||
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
|
||||
GLuint numlevels, GLuint minlayer, GLuint numlayers);
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
|
||||
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller) {
|
||||
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
|
||||
return true;
|
||||
}
|
||||
if (width != height) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array levels must be square (width == height)"));
|
||||
return false;
|
||||
}
|
||||
if (depth % 6 != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array depth must be a multiple of six"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
|
||||
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
|
||||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
|
||||
@@ -623,4 +645,144 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
|
||||
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
|
||||
// enum collapses every compressed format onto uncompressed storage and drops formats it
|
||||
// cannot carry, so classifying the converted value would silently widen the compatibility
|
||||
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case GL_RGBA32F:
|
||||
case GL_RGBA32UI:
|
||||
case GL_RGBA32I:
|
||||
return TextureViewClass::Bits128;
|
||||
case GL_RGB32F:
|
||||
case GL_RGB32UI:
|
||||
case GL_RGB32I:
|
||||
return TextureViewClass::Bits96;
|
||||
case GL_RGBA16F:
|
||||
case GL_RG32F:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RG32UI:
|
||||
case GL_RGBA16I:
|
||||
case GL_RG32I:
|
||||
case GL_RGBA16:
|
||||
case GL_RGBA16_SNORM:
|
||||
return TextureViewClass::Bits64;
|
||||
case GL_RGB16:
|
||||
case GL_RGB16_SNORM:
|
||||
case GL_RGB16F:
|
||||
case GL_RGB16UI:
|
||||
case GL_RGB16I:
|
||||
return TextureViewClass::Bits48;
|
||||
case GL_RG16F:
|
||||
case GL_R11F_G11F_B10F:
|
||||
case GL_R32F:
|
||||
case GL_RGB10_A2UI:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RG16UI:
|
||||
case GL_R32UI:
|
||||
case GL_RGBA8I:
|
||||
case GL_RG16I:
|
||||
case GL_R32I:
|
||||
case GL_RGB10_A2:
|
||||
case GL_RGBA8:
|
||||
case GL_RG16:
|
||||
case GL_RGBA8_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
case GL_SRGB8_ALPHA8:
|
||||
case GL_RGB9_E5:
|
||||
return TextureViewClass::Bits32;
|
||||
case GL_RGB8:
|
||||
case GL_RGB8_SNORM:
|
||||
case GL_SRGB8:
|
||||
case GL_RGB8UI:
|
||||
case GL_RGB8I:
|
||||
return TextureViewClass::Bits24;
|
||||
case GL_R16F:
|
||||
case GL_RG8UI:
|
||||
case GL_R16UI:
|
||||
case GL_RG8I:
|
||||
case GL_R16I:
|
||||
case GL_RG8:
|
||||
case GL_R16:
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R16_SNORM:
|
||||
return TextureViewClass::Bits16;
|
||||
case GL_R8UI:
|
||||
case GL_R8I:
|
||||
case GL_R8:
|
||||
case GL_R8_SNORM:
|
||||
return TextureViewClass::Bits8;
|
||||
case GL_COMPRESSED_RED_RGTC1:
|
||||
case GL_COMPRESSED_SIGNED_RED_RGTC1:
|
||||
return TextureViewClass::Rgtc1Red;
|
||||
case GL_COMPRESSED_RG_RGTC2:
|
||||
case GL_COMPRESSED_SIGNED_RG_RGTC2:
|
||||
return TextureViewClass::Rgtc2Rg;
|
||||
case GL_COMPRESSED_RGBA_BPTC_UNORM:
|
||||
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
|
||||
return TextureViewClass::BptcUnorm;
|
||||
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
|
||||
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
|
||||
return TextureViewClass::BptcFloat;
|
||||
default:
|
||||
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
|
||||
// reaches here. The caller must then demand an EXACT format match.
|
||||
return TextureViewClass::None;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
|
||||
switch (origTarget) {
|
||||
case TextureTarget::Texture1D:
|
||||
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
|
||||
case TextureTarget::Texture2D:
|
||||
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
|
||||
case TextureTarget::Texture3D:
|
||||
return viewTarget == TextureTarget::Texture3D;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureRectangle:
|
||||
return viewTarget == TextureTarget::TextureRectangle;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
|
||||
case TextureTarget::Texture2DArray:
|
||||
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
|
||||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
|
||||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return viewTarget == TextureTarget::Texture2DMultisample ||
|
||||
viewTarget == TextureTarget::Texture2DMultisampleArray;
|
||||
case TextureTarget::TextureBuffer:
|
||||
// The table lists no legal target for a buffer texture: its storage is a buffer
|
||||
// object, and there is nothing to make a view of.
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
|
||||
switch (viewTarget) {
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return 6;
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return 1;
|
||||
default:
|
||||
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
|
||||
// "multiple of 6" is checked by the caller).
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
|
||||
Bool ValidateTextureLevelNumber(Int level);
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
|
||||
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
|
||||
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
|
||||
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
|
||||
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
|
||||
// non-cube-array upload target answers true untouched.
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller);
|
||||
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
|
||||
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
|
||||
Bool ValidateTextureBorderNumber(Int border);
|
||||
@@ -79,4 +86,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||
// the requested internalformat asks for, but may supply more.
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||
|
||||
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
|
||||
// table, which the spec turns into a much stricter rule than "same class": such a format can
|
||||
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
|
||||
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
|
||||
enum class TextureViewClass {
|
||||
None = 0,
|
||||
Bits128,
|
||||
Bits96,
|
||||
Bits64,
|
||||
Bits48,
|
||||
Bits32,
|
||||
Bits24,
|
||||
Bits16,
|
||||
Bits8,
|
||||
Rgtc1Red,
|
||||
Rgtc2Rg,
|
||||
BptcUnorm,
|
||||
BptcFloat,
|
||||
};
|
||||
TextureViewClass GetTextureViewClass(GLenum internalformat);
|
||||
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
|
||||
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
|
||||
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
|
||||
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
|
||||
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
|
||||
// caller because its constraint is "a multiple of 6", not an exact count.
|
||||
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
Uint GetMaxVertexAttribs() {
|
||||
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!MG_Backend::pActiveBackendObject) return capacity;
|
||||
|
||||
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
|
||||
if (backendLimit <= 0) return capacity;
|
||||
return std::min(static_cast<Uint>(backendLimit), capacity);
|
||||
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
|
||||
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
|
||||
const Int backendLimit =
|
||||
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
|
||||
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribBindings() {
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.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
|
||||
|
||||
// The client side of the PipeInputs block (ARCHITECTURE.md 9.2 phase A): the only place in
|
||||
// the push arm that reads MG_State::pGLContext. Holds the per-verb filler, the F-class
|
||||
// forwarders, IsLive, the MOBILEGL_PIPE_POISON_OMIT knob and - in a verify build - the
|
||||
// second arm (SnapshotFromGLContext), the entry compare, the compare-at-read hook and the
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT / _FATAL knobs. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// (CMakeLists.txt appends it to SOURCE_FILES there).
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <Config.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// The one door into PipeInputs' storage on the client side. A struct rather than a
|
||||
// list of friend functions so the header names exactly one friend.
|
||||
struct MGPipeFillAccess {
|
||||
// Copies ONE field's storage out of the live context by calling the GLContext
|
||||
// accessor of the same name (P1 brief D4: no derivation logic is re-implemented
|
||||
// here, which is what keeps the copy semantically identical by construction).
|
||||
// A forwarded field has no storage and copies nothing.
|
||||
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
|
||||
using F = MGPipeInputField;
|
||||
using MG_State::GLState::BufferBindPointTargets;
|
||||
using MG_State::GLState::GlobalBufferTargets;
|
||||
switch (field) {
|
||||
case F::GetActiveTextureUnit:
|
||||
dst.m_activeTextureUnit = ctx.GetActiveTextureUnit();
|
||||
break;
|
||||
case F::GetBlendColor:
|
||||
dst.m_blendColor = ctx.GetBlendColor();
|
||||
break;
|
||||
case F::GetBlendEquationIndexed:
|
||||
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
||||
ctx.GetBlendEquationIndexed(i, dst.m_blendEquation[i][0], dst.m_blendEquation[i][1]);
|
||||
}
|
||||
break;
|
||||
case F::GetBlendFuncIndexed:
|
||||
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
||||
ctx.GetBlendFuncIndexed(i, dst.m_blendFunc[i][0], dst.m_blendFunc[i][1], dst.m_blendFunc[i][2],
|
||||
dst.m_blendFunc[i][3]);
|
||||
}
|
||||
break;
|
||||
case F::GetBoundTransformFeedbackName:
|
||||
dst.m_boundTransformFeedbackName = ctx.GetBoundTransformFeedbackName();
|
||||
break;
|
||||
case F::GetBoundVertexArray:
|
||||
dst.m_boundVertexArray = ctx.GetBoundVertexArray();
|
||||
break;
|
||||
case F::GetBufferBindingSlot:
|
||||
// Every global target has a slot; Index stays null - GLContext resolves it
|
||||
// through the bound VAO's element-buffer slot (Core.cpp), a derivation no
|
||||
// FillPoints.def row can copy - and a read of it is the poison Fatal in the
|
||||
// accessor. No backend reads it today (every slot read is DrawIndirect,
|
||||
// DispatchIndirect, Parameter or PixelPack).
|
||||
for (const auto target : GlobalBufferTargets) {
|
||||
dst.m_bufferBindingSlot[static_cast<SizeT>(target)] = &ctx.GetBufferBindingSlot(target);
|
||||
}
|
||||
break;
|
||||
case F::GetBufferBindingPoint:
|
||||
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
|
||||
// (BufferState.h), so the address of point 0 is the base of that target's row.
|
||||
for (const auto target : BufferBindPointTargets) {
|
||||
dst.m_bufferBindingPointBase[static_cast<SizeT>(target)] = &ctx.GetBufferBindingPoint(target, 0);
|
||||
}
|
||||
break;
|
||||
case F::GetTouchedBufferBindingPointCount:
|
||||
for (const auto target : BufferBindPointTargets) {
|
||||
dst.m_touchedBindingPointCount[static_cast<SizeT>(target)] =
|
||||
ctx.GetTouchedBufferBindingPointCount(target);
|
||||
}
|
||||
break;
|
||||
case F::GetClampReadColor:
|
||||
dst.m_clampReadColor = ctx.GetClampReadColor();
|
||||
break;
|
||||
case F::GetClearColor:
|
||||
dst.m_clearColor = ctx.GetClearColor();
|
||||
break;
|
||||
case F::GetClearDepth:
|
||||
dst.m_clearDepth = ctx.GetClearDepth();
|
||||
break;
|
||||
case F::GetClearStencil:
|
||||
dst.m_clearStencil = ctx.GetClearStencil();
|
||||
break;
|
||||
case F::GetColorMaskIndexed:
|
||||
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
||||
dst.m_colorMask[i] = ctx.GetColorMaskIndexed(i);
|
||||
}
|
||||
break;
|
||||
case F::GetCullFaceMode:
|
||||
dst.m_cullFaceMode = ctx.GetCullFaceMode();
|
||||
break;
|
||||
case F::GetCurrentVertexAttribute:
|
||||
for (Uint i = 0; i < PipeInputs::kMaxVertexAttribs; ++i) {
|
||||
dst.m_currentVertexAttribute[i] = ctx.GetCurrentVertexAttribute(i);
|
||||
}
|
||||
break;
|
||||
case F::GetDepthFunc:
|
||||
dst.m_depthFunc = ctx.GetDepthFunc();
|
||||
break;
|
||||
case F::GetDepthMask:
|
||||
dst.m_depthMask = ctx.GetDepthMask();
|
||||
break;
|
||||
case F::GetDepthRangeIndexed:
|
||||
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||
dst.m_depthRange[i] = ctx.GetDepthRangeIndexed(i);
|
||||
}
|
||||
break;
|
||||
case F::GetFramebufferBindingSlot:
|
||||
for (SizeT i = 0; i < PipeInputs::kFramebufferTargetCount; ++i) {
|
||||
dst.m_framebufferBindingSlot[i] =
|
||||
&ctx.GetFramebufferBindingSlot(static_cast<PipeInputs::FramebufferTarget>(i));
|
||||
}
|
||||
break;
|
||||
case F::GetImageTextureBinding:
|
||||
// Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0's
|
||||
// address is the base.
|
||||
dst.m_imageTextureBindingBase = &ctx.GetImageTextureBinding(0);
|
||||
break;
|
||||
case F::GetLineWidth:
|
||||
dst.m_lineWidth = ctx.GetLineWidth();
|
||||
break;
|
||||
case F::GetLogicOp:
|
||||
dst.m_logicOp = ctx.GetLogicOp();
|
||||
break;
|
||||
case F::GetMaxTouchedTextureUnit:
|
||||
dst.m_maxTouchedTextureUnit = ctx.GetMaxTouchedTextureUnit();
|
||||
break;
|
||||
case F::GetMinSampleShadingValue:
|
||||
dst.m_minSampleShadingValue = ctx.GetMinSampleShadingValue();
|
||||
break;
|
||||
case F::GetPatchDefaultInnerLevel:
|
||||
dst.m_patchDefaultInnerLevel = ctx.GetPatchDefaultInnerLevel();
|
||||
break;
|
||||
case F::GetPatchDefaultOuterLevel:
|
||||
dst.m_patchDefaultOuterLevel = ctx.GetPatchDefaultOuterLevel();
|
||||
break;
|
||||
case F::GetPatchVertices:
|
||||
dst.m_patchVertices = ctx.GetPatchVertices();
|
||||
break;
|
||||
case F::GetPipelineStateVersion:
|
||||
dst.m_pipelineStateVersion = ctx.GetPipelineStateVersion();
|
||||
break;
|
||||
case F::GetPixelStoreParameters:
|
||||
dst.m_pixelStore[0] = ctx.GetPixelStoreParameters(false);
|
||||
dst.m_pixelStore[1] = ctx.GetPixelStoreParameters(true);
|
||||
break;
|
||||
case F::GetPolygonModeFront:
|
||||
dst.m_polygonModeFront = ctx.GetPolygonModeFront();
|
||||
break;
|
||||
case F::GetPolygonOffsetFactor:
|
||||
dst.m_polygonOffsetFactor = ctx.GetPolygonOffsetFactor();
|
||||
break;
|
||||
case F::GetPolygonOffsetUnits:
|
||||
dst.m_polygonOffsetUnits = ctx.GetPolygonOffsetUnits();
|
||||
break;
|
||||
case F::GetPrimitiveRestartIndex:
|
||||
dst.m_primitiveRestartIndex = ctx.GetPrimitiveRestartIndex();
|
||||
break;
|
||||
case F::GetProgramForDispatch:
|
||||
dst.m_programForDispatch = ctx.GetProgramForDispatch();
|
||||
break;
|
||||
case F::GetProgramForDraw:
|
||||
dst.m_programForDraw = ctx.GetProgramForDraw();
|
||||
break;
|
||||
case F::GetProvokingVertexMode:
|
||||
dst.m_provokingVertexMode = ctx.GetProvokingVertexMode();
|
||||
break;
|
||||
case F::GetRenderStateParameters:
|
||||
dst.m_renderState = ctx.GetRenderStateParameters();
|
||||
break;
|
||||
case F::GetRenderStateParametersVersion:
|
||||
dst.m_renderStateParametersVersion = ctx.GetRenderStateParametersVersion();
|
||||
break;
|
||||
case F::GetSamplingResolutionGeneration:
|
||||
dst.m_samplingResolutionGeneration = ctx.GetSamplingResolutionGeneration();
|
||||
break;
|
||||
case F::GetScissorBox:
|
||||
dst.m_scissorBox = ctx.GetScissorBox();
|
||||
break;
|
||||
case F::GetStencilState:
|
||||
dst.m_stencil[0] = ctx.GetStencilState(StencilFace::Front);
|
||||
dst.m_stencil[1] = ctx.GetStencilState(StencilFace::Back);
|
||||
break;
|
||||
case F::GetTextureBindGeneration:
|
||||
dst.m_textureBindGeneration = ctx.GetTextureBindGeneration();
|
||||
break;
|
||||
case F::GetTextureContextId:
|
||||
dst.m_textureContextId = ctx.GetTextureContextId();
|
||||
break;
|
||||
case F::GetTextureUnitObject:
|
||||
// Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0 is the base.
|
||||
dst.m_textureUnitBase = &ctx.GetTextureUnitObject(0);
|
||||
break;
|
||||
case F::GetTransformFeedbackCapturedVertices:
|
||||
dst.m_transformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
|
||||
break;
|
||||
case F::GetTransformFeedbackGeneration:
|
||||
dst.m_transformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
|
||||
break;
|
||||
case F::GetTransformFeedbackPausedPrimitiveCounter:
|
||||
dst.m_transformFeedbackPausedPrimitiveCounter = ctx.GetTransformFeedbackPausedPrimitiveCounter();
|
||||
break;
|
||||
case F::GetTransformFeedbackProgram:
|
||||
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
|
||||
break;
|
||||
case F::GetViewport:
|
||||
dst.m_viewport = ctx.GetViewport();
|
||||
break;
|
||||
case F::GetViewportIndexed:
|
||||
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||
dst.m_viewportIndexed[i] = ctx.GetViewportIndexed(i);
|
||||
}
|
||||
break;
|
||||
case F::IsCapabilityEnabled:
|
||||
// Every capability, FramebufferSrgb included: it copies today's constant false
|
||||
// (MEASUREMENTS.md), so no value changes.
|
||||
for (SizeT i = 0; i < PipeInputs::kCapabilityCount; ++i) {
|
||||
dst.m_capability[i] = ctx.IsCapabilityEnabled(static_cast<CapabilityInput>(i));
|
||||
}
|
||||
break;
|
||||
case F::IsCapabilityEnabledIndexed:
|
||||
// The only two indexed capabilities GLContext keeps (RenderState).
|
||||
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
||||
dst.m_capabilityIndexed.Blend[i] = ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
|
||||
}
|
||||
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||
dst.m_capabilityIndexed.ScissorTest[i] =
|
||||
ctx.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i);
|
||||
}
|
||||
break;
|
||||
case F::IsTransformFeedbackActive:
|
||||
dst.m_transformFeedbackActive = ctx.IsTransformFeedbackActive();
|
||||
break;
|
||||
case F::IsTransformFeedbackPaused:
|
||||
dst.m_transformFeedbackPaused = ctx.IsTransformFeedbackPaused();
|
||||
break;
|
||||
case F::GetBoundTransformFeedbackLifetimeId:
|
||||
dst.m_boundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
|
||||
break;
|
||||
// The seven forwarded fields: nothing to copy.
|
||||
case F::GetBufferBindingPointCount:
|
||||
case F::GetProgramObject:
|
||||
case F::GetTextureObject:
|
||||
case F::HasOpenTransformFeedbackSpan:
|
||||
case F::InvalidateCompileEnv:
|
||||
case F::ValidateProgramName:
|
||||
case F::RecordError:
|
||||
case F::kFieldCount:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void SetIdentity(PipeInputs& inputs, GLContext* ctx) {
|
||||
inputs.m_live = ctx != nullptr;
|
||||
inputs.m_contextIdentity = ctx;
|
||||
}
|
||||
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
|
||||
#endif
|
||||
};
|
||||
|
||||
namespace {
|
||||
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
|
||||
|
||||
template <class T>
|
||||
const SharedPtr<T>& NullShared() {
|
||||
static const SharedPtr<T> null;
|
||||
return null;
|
||||
}
|
||||
|
||||
[[noreturn]] void BadKnob(const char* knob, const char* value, const char* why) {
|
||||
MGLOG_F("MGPipe: Fatal{PipeVerifyBadKnob, \"%s=%s\": %s}", knob, value, why);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// ---- MOBILEGL_PIPE_POISON_OMIT (negative control B, P1 brief D6) ----
|
||||
// The filler skips the STAMP (never the value) of one (verb, field) pair: an omission
|
||||
// indistinguishable from a forgotten FillPoints.def row, so that verb's read of the
|
||||
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected.
|
||||
struct PoisonOmission {
|
||||
Bool Armed = false;
|
||||
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
|
||||
MGPipeInputField Field = MGPipeInputField::kFieldCount;
|
||||
};
|
||||
PoisonOmission g_omission;
|
||||
Bool g_omissionKnobParsed = false;
|
||||
String g_omissionKnobValue; // the value the last parse saw
|
||||
|
||||
// Parsed on the first fill and again only when the value changes. A lane loads
|
||||
// Features once, before any fill, so that is one parse per process there; a forked
|
||||
// test child that sets Features after its parent already filled gets its own parse,
|
||||
// which is what puts the parser and its Fatal{PipeVerifyBadKnob} under a unit test.
|
||||
// An empty value never clears an omission a test armed through MGPipeSetPoisonOmission.
|
||||
void ParsePoisonOmissionKnob() {
|
||||
const String& knob = MG_Config::Features.PipePoisonOmit;
|
||||
if (g_omissionKnobParsed && knob == g_omissionKnobValue) return;
|
||||
g_omissionKnobParsed = true;
|
||||
g_omissionKnobValue = knob;
|
||||
if (knob.empty()) return;
|
||||
const auto colon = knob.find(':');
|
||||
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) {
|
||||
BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "expected <Verb>:<FieldName>");
|
||||
}
|
||||
const String verbName = knob.substr(0, colon);
|
||||
const String fieldName = knob.substr(colon + 1);
|
||||
const auto verb = MGPipeFindVerb(verbName.c_str());
|
||||
if (!verb) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such verb in kMGPipeVerbNames");
|
||||
const auto field = MGPipeFindInputField(fieldName.c_str());
|
||||
if (!field) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such field in kMGPipeInputFieldNames");
|
||||
MGPipeSetPoisonOmission(verbName.c_str(), fieldName.c_str());
|
||||
}
|
||||
|
||||
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
|
||||
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
|
||||
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
|
||||
// once per verb (the pushed block against a second snapshot of the live context,
|
||||
// taken at the same instant - tautological until P2 gives the first arm a real
|
||||
// filler, and kept falsifiable by MOBILEGL_PIPE_VERIFY_CORRUPT), and the
|
||||
// COMPARE-AT-READ in every accessor (the stored value against a fresh read of the
|
||||
// live context at the moment the backend reads it - the arm that is real in P1: it
|
||||
// catches a value that changed between the verb boundary and the read).
|
||||
PipeInputs g_snapshot{}; // the second arm
|
||||
PipeInputs g_readScratch{}; // where the compare-at-read re-read lands
|
||||
|
||||
// The read hook arms at the first fill (ArmVerify below), so it cannot see a read
|
||||
// made before that. That window is covered by the poison instead: MGP_INPUT_CHECK
|
||||
// precedes MGP_INPUT_VERIFY_READ in every accessor and a stamp of 0 is never fresh,
|
||||
// so such a read is Fatal{UnmigratedPipeInput, "<Field>@<none>"} before the hook
|
||||
// could matter - which holds only while a verify build always carries the poison.
|
||||
static_assert(MOBILEGL_PIPE_POISON, "the compare-at-read hook relies on the poison for reads before the first fill");
|
||||
|
||||
struct VerifyState {
|
||||
Bool Parsed = false;
|
||||
Bool Enabled = false;
|
||||
Bool Fatal = true;
|
||||
Bool InHook = false; // a re-read that re-enters an accessor is not re-verified
|
||||
Optional<MGPipeInputField> Corrupt;
|
||||
String CorruptKnob; // the MOBILEGL_PIPE_VERIFY_CORRUPT value the last arm saw
|
||||
std::atomic<Uint64> Divergences{0};
|
||||
~VerifyState() {
|
||||
const Uint64 count = Divergences.load(std::memory_order_relaxed);
|
||||
if (count != 0) {
|
||||
MGLOG_E("MGPipe: verify summary - %llu divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0",
|
||||
static_cast<unsigned long long>(count));
|
||||
}
|
||||
}
|
||||
};
|
||||
VerifyState g_verify;
|
||||
|
||||
// Armed on the first fill and re-armed when any of the three verify knobs'
|
||||
// Features value (PipeVerify, PipeVerifyFatal, PipeVerifyCorrupt) differs from what
|
||||
// the last arm latched (the same reason as ParsePoisonOmissionKnob: one arm per lane
|
||||
// process, a fresh arm for a forked test child that turns a knob after its parent
|
||||
// filled). Cost: two Bool compares and one String compare per fill, verify builds only.
|
||||
void ArmVerify() {
|
||||
const auto& features = MG_Config::Features;
|
||||
if (g_verify.Parsed && g_verify.Enabled == features.PipeVerify && g_verify.Fatal == features.PipeVerifyFatal &&
|
||||
g_verify.CorruptKnob == features.PipeVerifyCorrupt) {
|
||||
return;
|
||||
}
|
||||
g_verify.Parsed = true;
|
||||
g_verify.Enabled = features.PipeVerify;
|
||||
g_verify.Fatal = features.PipeVerifyFatal;
|
||||
g_verify.CorruptKnob = features.PipeVerifyCorrupt;
|
||||
g_verify.Corrupt = Optional<MGPipeInputField>{};
|
||||
if (!g_verify.Enabled) return;
|
||||
const String& corrupt = g_verify.CorruptKnob;
|
||||
if (!corrupt.empty()) {
|
||||
const auto field = MGPipeFindInputField(corrupt.c_str());
|
||||
if (!field) {
|
||||
BadKnob("MOBILEGL_PIPE_VERIFY_CORRUPT", corrupt.c_str(), "no such field in kMGPipeInputFieldNames");
|
||||
}
|
||||
g_verify.Corrupt = field;
|
||||
}
|
||||
// The lanes grep for this line: a verify run whose log lacks it never armed.
|
||||
MGLOG_I("MGPipe: verify armed - %u fields, %u verbs, fatal=%d", static_cast<unsigned>(kMGPipeInputFieldCount),
|
||||
static_cast<unsigned>(kMGPipeVerbCount), g_verify.Fatal ? 1 : 0);
|
||||
if (g_verify.Corrupt) {
|
||||
MGLOG_I("MGPipe: verify corruption armed - %s", kMGPipeInputFieldNames[static_cast<SizeT>(*g_verify.Corrupt)]);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportDivergence(MGPipeInputField field, const char* where) {
|
||||
const Uint64 serial = MGPipeFillAccess::Filled(gPipeInputs).CurrentVerbSerial;
|
||||
MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(gPipeInputs.CurrentVerb()),
|
||||
static_cast<unsigned long long>(serial), where);
|
||||
if (g_verify.Fatal) std::abort();
|
||||
g_verify.Divergences.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void EntryCompare(PipeInputs& inputs, const MGPipeFieldMask& mask) {
|
||||
if (!g_verify.Enabled) return;
|
||||
SnapshotFromGLContext(g_snapshot, mask);
|
||||
// Negative control A: perturb the SNAPSHOT arm, so a green run goes red naming the
|
||||
// field. A field outside this verb's mask is not compared and stays untouched.
|
||||
if (g_verify.Corrupt && MGPipeFieldMaskHas(mask, *g_verify.Corrupt)) {
|
||||
MGPipeApplyVerifyCorruption(g_snapshot, *g_verify.Corrupt);
|
||||
}
|
||||
MGPipeInputField differing = MGPipeInputField::kFieldCount;
|
||||
if (!MGPipeVerifyInputs(inputs, g_snapshot, mask, &differing)) ReportDivergence(differing, "entry");
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
} // namespace
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask) {
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(snapshot, ctx);
|
||||
MGPipeFillAccess::SetVerb(snapshot, gPipeInputs.CurrentVerb());
|
||||
if (ctx == nullptr) return;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field) || kMGPipeInputFieldSticky[i]) continue;
|
||||
MGPipeFillAccess::CopyField(snapshot, *ctx, field);
|
||||
}
|
||||
}
|
||||
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
|
||||
if (&self != &gPipeInputs || !g_verify.Enabled || g_verify.InHook) return;
|
||||
const auto index = static_cast<SizeT>(field);
|
||||
if (kMGPipeInputFieldSticky[index]) return;
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) return;
|
||||
// The whole field is re-read and compared - a superset of "the same indices", so a
|
||||
// divergence in an index the backend did not ask for is still a divergence between
|
||||
// the boundary value and the live value. The indices only decorate the report. The
|
||||
// cost is per backend read (GetRenderStateParameters re-copies and compares the whole
|
||||
// struct; GetProgramForDraw re-joins the pending link), inside the verify budget and
|
||||
// to be kept in mind when reading the verify lane's wall time.
|
||||
// InHook: the re-read calls the same GLContext accessor the filler calls, and
|
||||
// GetProgramForDraw's join can re-enter a backend and with it another gPipeInputs
|
||||
// accessor; that inner read is a plain load rather than a second hook, so the hook
|
||||
// never recurses (and never reports the inner read against a half-copied scratch).
|
||||
g_verify.InHook = true;
|
||||
MGPipeFillAccess::CopyField(g_readScratch, *ctx, field);
|
||||
const Bool equal = MGPipeInputsFieldEqual(field, self, g_readScratch);
|
||||
g_verify.InHook = false;
|
||||
if (equal) return;
|
||||
MGLOG_E("MGPipe: verify read of %s (index %u, %u) differs from the live context", kMGPipeInputFieldNames[index],
|
||||
index0, index1);
|
||||
ReportDivergence(field, "read");
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
|
||||
// ---- push on mutation (P1 lane finding F2) ----
|
||||
// A backend that writes a frontend object inside its own verb moves a value the verb
|
||||
// boundary already copied: Magma's ResolveSamplerDescriptor synthesises a fallback
|
||||
// texture for an unbound sampler and its AllocateStorage/SetInternalFormat bump the
|
||||
// context's sampling-resolution generation, so every read of that field after the
|
||||
// fallback differs from the live context (the two SampledSetStaleness / six
|
||||
// UnboundImageDescriptor entries the verify lane aborted on). The frontend mutator
|
||||
// spells MGP_NOTE_MUTATION(Field) at the point of the move and lands here.
|
||||
//
|
||||
// Only the value is refreshed. The stamp is deliberately left alone: a field whose stamp
|
||||
// this verb withheld (negative control B) must stay stale, and a field the verb never
|
||||
// filled must stay Fatal{UnmigratedPipeInput} on the next read rather than be healed by
|
||||
// an unrelated frontend write.
|
||||
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) return;
|
||||
const auto verb = inputs.CurrentVerb();
|
||||
if (verb == MGPipeVerb::kVerbCount) return; // nothing has filled the block yet
|
||||
const auto index = static_cast<SizeT>(field);
|
||||
if (kMGPipeInputFieldSticky[index]) return; // forwarded: no storage to refresh
|
||||
const MGPipeFieldMask& mask =
|
||||
kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
|
||||
if (!MGPipeFieldMaskHas(mask, field)) return; // this verb never pushed it
|
||||
MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
}
|
||||
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||
if (verb == nullptr || field == nullptr) {
|
||||
g_omission = PoisonOmission{};
|
||||
return;
|
||||
}
|
||||
const auto v = MGPipeFindVerb(verb);
|
||||
const auto f = MGPipeFindInputField(field);
|
||||
if (!v || !f) BadKnob("MOBILEGL_PIPE_POISON_OMIT", verb, "unknown verb or field");
|
||||
g_omission.Armed = true;
|
||||
g_omission.Verb = *v;
|
||||
g_omission.Field = *f;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGLOG_I("MGPipe: poison omission armed - %s@%s", field, verb);
|
||||
#else
|
||||
MGLOG_W_ONCE("MGPipe: poison omission %s@%s requested but the poison is not compiled in "
|
||||
"(MOBILEGL_PIPE_POISON=0): no stamp exists to omit",
|
||||
field, verb);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- liveness ----
|
||||
Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }
|
||||
|
||||
// ---- the seven F-class forwarders ----
|
||||
SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const {
|
||||
const auto* ctx = LiveContext();
|
||||
return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0;
|
||||
}
|
||||
|
||||
const SharedPtr<PipeInputs::ProgramObject>& PipeInputs::GetProgramObject(Uint index) {
|
||||
auto* ctx = LiveContext();
|
||||
return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared<ProgramObject>();
|
||||
}
|
||||
|
||||
const SharedPtr<PipeInputs::ITextureObject>& PipeInputs::GetTextureObject(Uint index) {
|
||||
auto* ctx = LiveContext();
|
||||
return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared<ITextureObject>();
|
||||
}
|
||||
|
||||
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
|
||||
const auto* ctx = LiveContext();
|
||||
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
|
||||
}
|
||||
|
||||
void PipeInputs::InvalidateCompileEnv() {
|
||||
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
|
||||
}
|
||||
|
||||
Bool PipeInputs::ValidateProgramName(Uint index) const {
|
||||
const auto* ctx = LiveContext();
|
||||
return ctx != nullptr && ctx->ValidateProgramName(index);
|
||||
}
|
||||
|
||||
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) {
|
||||
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
|
||||
return;
|
||||
}
|
||||
ctx->RecordError(code, Move(info));
|
||||
}
|
||||
|
||||
void MGPipeLeaveVerb() {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// Same bump the next fill would make, without a verb to fill from: no field is
|
||||
// stamped, so every stamp this verb made falls behind the serial.
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
}
|
||||
|
||||
// ---- the filler ----
|
||||
void MGPipeFillForVerb(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
ParsePoisonOmissionKnob();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
ArmVerify();
|
||||
#else
|
||||
// The runtime knob without the compiled comparator is a no-op that would look green;
|
||||
// this warning is what a lane's arming assertion turns into red.
|
||||
if (MG_Config::Features.PipeVerify) {
|
||||
MGLOG_W_ONCE("MGPipe: MOBILEGL_PIPE_VERIFY=1 requested but the comparator is not compiled in "
|
||||
"(configure with -DMOBILEGL_PIPE_VERIFY=ON)");
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
|
||||
// Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh refuses
|
||||
// it on both branches, so a read before this first bump is
|
||||
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
|
||||
++filled.CurrentVerbSerial;
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, verb);
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(inputs, ctx);
|
||||
if (ctx == nullptr) return;
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
if (kMGPipeInputFieldSticky[i]) {
|
||||
// Stamped once by the first fill that sees a live context; fresh through the
|
||||
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
|
||||
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
if (kMGPipeInputFieldSticky[i]) continue;
|
||||
#endif
|
||||
MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// The value is copied either way; only the stamp is withheld for the omitted pair.
|
||||
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
|
||||
#endif
|
||||
}
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
EntryCompare(inputs, mask);
|
||||
#endif
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,55 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
// The fill point (ARCHITECTURE.md 9.2, P1 brief D7). MG_Impl spells MGP_FILL(Verb); as the
|
||||
// statement immediately before every call through gBackendFunctionsTable.GL - after every
|
||||
// early return the call is behind, inside the loop body for a call made in a loop - so the
|
||||
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
|
||||
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
|
||||
// it.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct PipeInputs;
|
||||
|
||||
// PipeFill.cpp. Bumps the per-verb serial, records the verb and the context identity,
|
||||
// and copies every field in the verb class's may-read mask (kMGPipeClassFieldMask) out
|
||||
// of the live GLContext, stamping each with the new serial. In a verify build it then
|
||||
// runs the entry compare against a second snapshot (P1 brief D8).
|
||||
void MGPipeFillForVerb(MGPipeVerb verb);
|
||||
|
||||
// Ends the verb in flight without starting another: bumps the serial, so every field the
|
||||
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
||||
// it aborts as Fatal{UnmigratedPipeInput, "<Field>@<none>"} - which is what such a read
|
||||
// is - instead of naming whichever verb happened to be filled last. Nothing in the GL
|
||||
// entry points calls this: a real verb is always followed by the next verb's fill. It
|
||||
// exists for a caller that drives a backend helper directly and wants its declaration to
|
||||
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
||||
void MGPipeLeaveVerb();
|
||||
|
||||
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
|
||||
// the value - of `field` at `verb`, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
|
||||
// MOBILEGL_PIPE_POISON_OMIT knob ("<Verb>:<FieldName>") calls this once, on the first
|
||||
// fill; tests call it directly. Both null clears the omission. An unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field);
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// PipeFill.cpp. The second arm of the comparator (P1 brief D8, ARCHITECTURE.md 13.2-2):
|
||||
// fills `snapshot` from the live GLContext the old way, for every field in `mask`. This
|
||||
// is the branch that survives P13, which is why it is its own function rather than the
|
||||
// filler's loop.
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
|
||||
#else
|
||||
#define MGP_FILL(Verb) ((void)0)
|
||||
#endif
|
||||
@@ -51,6 +51,7 @@ endif()
|
||||
add_executable(MobileGLIntegrationTest
|
||||
Main.cpp
|
||||
Harness/HeadlessGL.cpp
|
||||
Harness/BackendCapsPeek.cpp
|
||||
Scenarios/OrientationScenario.cpp
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
@@ -58,13 +59,18 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DrawParametersScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
Scenarios/SampleMaskScopeScenario.cpp
|
||||
Scenarios/SampledSetStalenessScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/SnormAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
Scenarios/AdvertisedLimitsScenario.cpp
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/PrimitiveRestartScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/SampleVariablesScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
@@ -86,6 +92,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||
Scenarios/UnlocatedIoBlockScenario.cpp
|
||||
Scenarios/TessellationDrawModeScenario.cpp
|
||||
Scenarios/GeometryDrawModeScenario.cpp
|
||||
Scenarios/PostLinkAttachScenario.cpp
|
||||
@@ -95,16 +102,33 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||
Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||
Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
Scenarios/PointSizeDemotionScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/CopyImagePacked16Scenario.cpp
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredAttachmentShapeScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/LargeArenaAdoptionScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/SpirvShaderBinaryScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
Scenarios/GuiBatchScenario.cpp
|
||||
Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/PipeVerifyArmingScenario.cpp
|
||||
Scenarios/PoisonOmissionScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -270,9 +294,9 @@ if (MOBILEGL_ITEST_VK_ICD)
|
||||
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
|
||||
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
|
||||
list(APPEND MGL_ITEST_VULKAN_ENV
|
||||
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
|
||||
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
|
||||
"MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
|
||||
"MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1"
|
||||
"MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -303,6 +327,70 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# The shader-compiler configurations AsyncCompileScenario needs, and the one
|
||||
# ViewportArrayScenario's negative control needs.
|
||||
#
|
||||
# These used to be poked into MG_Config::Features from inside the test bodies. They
|
||||
# cannot be any more - on Android this module links the SHIPPING libMobileGL.so, which
|
||||
# exports nothing internal - and they should not have been anyway: half of what each of
|
||||
# them decides is latched before the first GL call (the compile pool and its threads;
|
||||
# the advertised extension list, which a backend builds once from the configuration in
|
||||
# force at its first use), so an in-process write could only ever have moved the other
|
||||
# half. Every one of them is a whole-process property, and a whole-process property is
|
||||
# spelled with an environment variable and a ctest entry of its own.
|
||||
#
|
||||
# Note the shape of every list here: it APPENDS to MGL_ITEST_COMMON_ENV /
|
||||
# MGL_ITEST_VULKAN_ENV rather than standing alone. A ctest ENVIRONMENT property REPLACES
|
||||
# the job environment rather than adding to it, so an entry that lists only its mode
|
||||
# variable would silently lose the EGL vendor and Vulkan ICD pinning and run against
|
||||
# whatever the loader found first.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
# MOBILEGL_LOG_FILE_PATH alongside the pin, because the arming assertion needs somewhere to
|
||||
# read the library's own report from. The strip's arming signal is a latched MGLOG_I and there
|
||||
# is no other way for a test process to learn that it fired - MG_Config is not reachable from
|
||||
# this module on Android, where it links the shipping library. The path is per-lane so nothing
|
||||
# else appends to it, and the case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
|
||||
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
|
||||
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
|
||||
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
|
||||
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
|
||||
# SPIR-V Xfb decorations to the carrier).
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
|
||||
@@ -367,3 +455,359 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
|
||||
# reason the depth/stencil entry above pins its emulation: without it this scenario is
|
||||
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
|
||||
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
|
||||
# emulation never runs - the ambient registration would be exercising the un-stripped path
|
||||
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
|
||||
# location and the assertion is about the spelling the device gets.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
|
||||
TEST_FILTER "UnlocatedIoBlockScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||
#
|
||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||
# MobileGL's built-in default happens to be, and the day that default flips they would
|
||||
# stop covering the asynchronous path without anything going red. These entries are the
|
||||
# ones that keep the asynchronous half tested no matter what ships. They are also the
|
||||
# only place ExtensionStringMatchesTheConfiguration can assert that the extension IS
|
||||
# advertised - the case derives its expectation from this variable and nothing else, and
|
||||
# skips where it is unset, precisely so that it is not asserting the implementation
|
||||
# against itself.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOn."
|
||||
TEST_FILTER "AsyncCompileScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The other side of the same switch: asynchronous compilation OFF, so
|
||||
# GL_KHR_parallel_shader_compile must be WITHDRAWN from both spellings of the extension
|
||||
# list and GL_MAX_SHADER_COMPILER_THREADS_KHR must read 0. Only that one case is
|
||||
# registered here because it is the only one that has anything to say in this
|
||||
# configuration - the other four exist to observe worker-built artifacts, and there are
|
||||
# none - so registering the whole scenario would buy four guaranteed skips per backend.
|
||||
# Together with the AsyncOn. entries above, one ctest run still covers both flag states,
|
||||
# which is what the in-process forcing used to be for.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.AsyncOff."
|
||||
TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The optimistic-status quirk's end-to-end shape. Its own entries and not part of the
|
||||
# AsyncOn. ones because the quirk is not neutral for the rest of the scenario: with it in
|
||||
# force glGetShaderiv(GL_COMPILE_STATUS) deliberately answers without joining, which is
|
||||
# exactly what CompletionStatusPollingThenForcedJoin asserts must NOT happen. Off by
|
||||
# default and never advertised, so - unlike asynchronous compilation, which announces
|
||||
# itself through the extension string - the variable is the only thing that can tell the
|
||||
# case it is in force.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.OptimisticShaderStatus."
|
||||
TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The negative control for the DirectGLES gl_ViewportIndex emulation, in a process that
|
||||
# has it switched off. One case, because it is the only one the switch may touch: with
|
||||
# the emulation off the three positive cases in the same fixture describe behaviour the
|
||||
# backend does not have, so a whole-scenario registration would be three guaranteed reds.
|
||||
# DirectGLES only - the flag steers nothing on DirectVulkan, which routes natively.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.NoViewportArrayEmulation."
|
||||
TEST_FILTER "ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
|
||||
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
|
||||
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
|
||||
# the same GL questions and must produce the same numbers - the "two pools must agree"
|
||||
# gate this machine can hold that the affected device cannot. The pinned entry is also
|
||||
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
|
||||
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
|
||||
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
|
||||
# steers nothing on DirectGLES.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PrimGenReroute."
|
||||
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
|
||||
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
|
||||
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
|
||||
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
|
||||
# DirectVulkan, which has always stored these formats widened.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.WidenedPacked16."
|
||||
TEST_FILTER "CopyImagePacked16Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
|
||||
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
|
||||
# geometry stages, so the ambient registrations run these captures through the built-in and
|
||||
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
|
||||
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
|
||||
# path. Both backends, because the demotion is shared phase-B work with two different
|
||||
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- the third CI mode: MOBILEGL_PIPE_VERIFY -----------------------------------
|
||||
#
|
||||
# ARCHITECTURE.md 13.2-(2) asks for a THIRD build mode next to pull and push: two state models in
|
||||
# one address space, compared field by field at every verb boundary and again at every accessor
|
||||
# read, 5-10x slower and never shipped. These entries are that mode's lane. They exist only when
|
||||
# the library was configured with -DMOBILEGL_PIPE_VERIFY=ON, which is deliberate and is half of
|
||||
# what makes the lane falsifiable: `ctest -L integration-verify --no-tests=error` in a build that
|
||||
# forgot the option matches NO tests and fails, instead of reporting a green run of nothing.
|
||||
#
|
||||
# The other half is PipeVerifyArmingScenario.Armed, which asserts the library's own arming line -
|
||||
# because MOBILEGL_PIPE_VERIFY=1 in the environment of a library that never compiled the
|
||||
# comparator in is a silent no-op that looks exactly like a clean pass.
|
||||
#
|
||||
# Three things about the ENVIRONMENT properties below, each of which has already gone wrong once
|
||||
# in this file:
|
||||
# * every list APPENDS ${MGL_ITEST_COMMON_ENV} / ${MGL_ITEST_VULKAN_ENV}. A ctest ENVIRONMENT
|
||||
# entry overrides the job environment for the names it lists, so an entry that named only its
|
||||
# own knobs would lose the EGL vendor and Vulkan ICD pinning and run against whichever driver
|
||||
# the loader found first.
|
||||
# * the ambient Verify. entries name NEITHER MOBILEGL_PIPE_VERIFY_CORRUPT NOR
|
||||
# MOBILEGL_PIPE_POISON_OMIT. That is what lets CI's two always-on negative-control steps
|
||||
# export those knobs in the JOB environment and have them reach the test processes; a
|
||||
# property entry of the same name would silently win and the controls would prove nothing.
|
||||
# * MOBILEGL_LOG_FILE_PATH is per lane, and "per lane" is the exact limit of what it proves. It
|
||||
# is the only channel a test process has for reading the library's own report (MG_Config is not
|
||||
# reachable from this module), but the log is opened fopen(path, "w"), so every process in a
|
||||
# lane TRUNCATES it: after an ambient lane of 400-odd entries the file holds the LAST process
|
||||
# and nothing else. Reading it is therefore only sound in a filtered, one-entry lane - which is
|
||||
# why the arming case has a lane and a log of its own below, and why neither this file nor CI
|
||||
# may read the ambient logs as evidence about the entries that ran before the last one. The
|
||||
# ambient path is kept for post-mortems (and to keep library chatter out of ctest's capture).
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
|
||||
# boundary and again at every accessor read, which the design budgets at 5-10x.
|
||||
set(MGL_ITEST_VERIFY_TIMEOUT 900)
|
||||
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# The arming assertion's own lane, one case per backend, with a log path nothing else writes to.
|
||||
#
|
||||
# PipeVerifyArmingScenario.Armed reads the library's log, and the log is a per-LANE resource: it
|
||||
# is opened fopen(path, "w"), so every process in a lane truncates it. In the ambient Verify.
|
||||
# lane that is 400-odd processes on one path, run `-j 4` in CI, and a whole-file read there
|
||||
# races a neighbour's bring-up. Every other log-reading scenario in this file (UnlocatedIoBlocks,
|
||||
# the primgen reroute, the point-size demotion) is registered exactly like this for the same
|
||||
# reason. MGITEST_PIPE_ARMING_LANE is a harness marker - the library never reads it - and it is
|
||||
# what makes the case skip in the ambient lane instead of racing there.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# Negative control A (G4). MOBILEGL_PIPE_VERIFY_FATAL=0 so the process SURVIVES its own
|
||||
# divergence and the case can read the report back out of the log; the CI step that exports
|
||||
# the same corruption against the ambient lane, where FATAL keeps its default of 1, asserts
|
||||
# the other half - that a divergence aborts and reds the entry.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# Negative control B (G5). The omission skips the STAMP of one field for one verb while still
|
||||
# copying its value, which is indistinguishable from a fill row nobody wrote; the scenario
|
||||
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# The whole suite again, per backend, with the comparator armed. Same scenarios, same
|
||||
# assertions, but every backend read of frontend state is now checked against a snapshot taken
|
||||
# from the live context at the verb boundary - which is what "the 742 integration entries
|
||||
# prove push equals pull" means. Labelled integration-gpu as well so a verify build's
|
||||
# `ctest -L integration-gpu` still describes the whole registration set.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.Verify."
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.Verify."
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The arming assertion, one entry per backend. This is the entry that fails a lane whose library
|
||||
# never armed: it runs the same library and the same MOBILEGL_PIPE_VERIFY=1 as the ambient
|
||||
# entries above, but unlike them it cannot be green against a library with no comparator
|
||||
# compiled in. Its log is its own, so `-j 4` cannot make it flake.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.VerifyArming."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.Armed"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.VerifyArming."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.Armed"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# One case each: the knobs are process-wide, so a corrupted or poisoned process cannot also be
|
||||
# running the ambient assertions. These four entries are the ones that assert the RED - they
|
||||
# pass when the comparator and the poison report, and go red when either stops.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.VerifyCorrupted."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.VerifyCorrupted."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.PoisonOmitted."
|
||||
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PoisonOmitted."
|
||||
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "BackendCapsPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// Declared in MG_Backend/BackendObjects.h, which also pulls in both backends' headers
|
||||
// and, through them, their loaders; the reference alone is all that is needed here.
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
} // namespace MobileGL::MG_Backend
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]) {
|
||||
#if defined(__ANDROID__)
|
||||
(void)outCount;
|
||||
(void)outSize;
|
||||
return false;
|
||||
#else
|
||||
const auto& backend = MobileGL::MG_Backend::pActiveBackendObject;
|
||||
if (!backend) {
|
||||
return false;
|
||||
}
|
||||
const MobileGL::MG_Backend::DynamicBackendParameters& caps = backend->GetDynamicParameters();
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
outCount[axis] = caps.MaxComputeWorkGroupCount[axis];
|
||||
outSize[axis] = caps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,29 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// The one place this module looks past the GL API into the active backend's caps block.
|
||||
//
|
||||
// It exists for exactly one assertion: that the six per-axis compute limits the MGPipe
|
||||
// caps block carries (DynamicBackendParameters::MaxComputeWorkGroupCount/Size, plan B
|
||||
// section 4.4.1) are the same numbers glGetIntegeri_v answers today, since P0.5 retires
|
||||
// the getter in favour of the caps. A separate translation unit, because the scenario
|
||||
// sources include the GL headers with prototypes and MobileGL's umbrella header is not
|
||||
// meant to meet them in one file.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// Copies the active backend's MaxComputeWorkGroupCount / MaxComputeWorkGroupSize into the
|
||||
// two arrays and returns true. Returns false, touching nothing, where the caps block is
|
||||
// out of reach: on Android this module links the SHIPPING libMobileGL.so, built
|
||||
// -fvisibility=hidden, so no internal symbol resolves; on desktop it links MobileGL_s and
|
||||
// the read is direct.
|
||||
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -552,7 +552,15 @@ namespace MGITest {
|
||||
// before the pre-flight forks - the child must measure the same platform
|
||||
// the parent will use.
|
||||
EnsureHeadlessPlatform();
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||
// The backend that is actually about to come up, which is what every
|
||||
// `BackendName() == "DirectGLES"` gate in the scenarios means by the question.
|
||||
// MG_ConfigLoader::InitBackendType defaults an unset MOBILEGL_BACKEND_TYPE to
|
||||
// DirectGLES, so the same default belongs here; this used to report the literal
|
||||
// "<unset>" instead. Under ctest the variable is always set by the ENVIRONMENT
|
||||
// property, which is why that never showed - but run straight from a device
|
||||
// shell, where nothing sets it, DirectGLES came up and every case gated on the
|
||||
// NAME DirectGLES skipped as though it had not.
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "DirectGLES");
|
||||
m_usable = BringUp();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,49 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "HeadlessGL.h"
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
// How a MOBILEGL_* quirk variable reads in THIS process's environment.
|
||||
//
|
||||
// A scenario that needs a non-default configuration takes it from here and skips
|
||||
// when the process it was launched into is not in that configuration, rather than
|
||||
// writing MG_Config::Features itself. Two reasons, and the second one decides it:
|
||||
//
|
||||
// - the feature table is an internal symbol. On Android this module links against
|
||||
// the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the
|
||||
// real artifact - and that library is built -fvisibility=hidden, so nothing
|
||||
// internal is reachable from here at all.
|
||||
// - a quirk poked in-process is already too late for everything latched at
|
||||
// initialization: the compile pool and its threads, and the backend's advertised
|
||||
// extension list, which is built once from the configuration in force at first
|
||||
// use. The process-wide variable is the only spelling that covers the whole
|
||||
// configuration instead of the half of it that is still mutable afterwards.
|
||||
//
|
||||
// The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp,
|
||||
// QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a
|
||||
// built-in default, i.e. a value only the implementation knows - a truthy value is
|
||||
// On, and anything else that IS set ("0", "false", "") is Off.
|
||||
enum class AmbientQuirk { Auto, On, Off };
|
||||
|
||||
inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr) return AmbientQuirk::Auto;
|
||||
std::string lowered(value);
|
||||
for (char& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
|
||||
return AmbientQuirk::On;
|
||||
}
|
||||
|
||||
class ScenarioTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
|
||||
// fails on both is a table bug.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/BackendCapsPeek.h"
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
@@ -56,7 +58,13 @@ namespace MGITest {
|
||||
|
||||
const std::vector<LimitBound>& BufferLimitTable() {
|
||||
static const std::vector<LimitBound> table = {
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
||||
// 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
|
||||
// layer's indexed-binding array - the two were made to coincide when the array
|
||||
// was widened from 36, which had made the clamp in GL_Getter degenerate.
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
|
||||
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
|
||||
// three, and the two tessellation stages were simply missing from it.
|
||||
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
@@ -133,6 +141,49 @@ namespace MGITest {
|
||||
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
|
||||
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
|
||||
}
|
||||
|
||||
// THE MIDDLE TERM, which the relation quoted above always had and this case never
|
||||
// checked. It is the one that actually broke: widening the binding-point array to 84
|
||||
// raised what every PER-STAGE count clamps to, while the combined value was a
|
||||
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
|
||||
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
|
||||
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
|
||||
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
|
||||
struct StageAgainstCombined {
|
||||
GLenum stage;
|
||||
const char* stageName;
|
||||
GLenum combined;
|
||||
const char* combinedName;
|
||||
};
|
||||
const StageAgainstCombined stageRelations[] = {
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
};
|
||||
for (const StageAgainstCombined& relation : stageRelations) {
|
||||
GLint stage = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(relation.stage, &stage);
|
||||
glGetIntegerv(relation.combined, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
|
||||
EXPECT_LE(stage, combined)
|
||||
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
|
||||
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
|
||||
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
|
||||
@@ -199,6 +250,80 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
|
||||
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
|
||||
// too-small value against a context advertising 4.6, and each is the FIRST call its
|
||||
// conformance case makes - so the case died before it could measure anything.
|
||||
//
|
||||
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
|
||||
// cull-distance route MUST report it), so it is checked for answerability only, below.
|
||||
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
|
||||
const std::vector<LimitBound> table = {
|
||||
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
|
||||
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
|
||||
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
|
||||
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
|
||||
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
|
||||
// the number un-gates two transform-feedback CTS cases per package across
|
||||
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
|
||||
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
|
||||
// a row here would pin a number the implementation cannot back.
|
||||
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
|
||||
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
|
||||
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
|
||||
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
|
||||
58368, 1 << 30},
|
||||
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
|
||||
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
|
||||
};
|
||||
for (const LimitBound& bound : table) {
|
||||
GLint value = -424242;
|
||||
glGetIntegerv(bound.pname, &value);
|
||||
const unsigned int error = FirstGLError();
|
||||
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
|
||||
if (error != GL_NO_ERROR) continue;
|
||||
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
|
||||
<< bound.minimum;
|
||||
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
|
||||
<< bound.ceiling;
|
||||
}
|
||||
|
||||
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
|
||||
// so only answerability and the combined-limit ordering are checked here.
|
||||
GLint cull = -1;
|
||||
GLint clip = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
|
||||
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
|
||||
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
|
||||
EXPECT_GE(cull, 0);
|
||||
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
|
||||
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
|
||||
|
||||
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
|
||||
// the wide query must answer it and the narrow one must saturate rather than wrap.
|
||||
GLint64 elementIndex = -1;
|
||||
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
|
||||
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
@@ -255,5 +380,250 @@ namespace MGITest {
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
|
||||
// THE INDEXED AND PER-PROGRAM QUERIES THAT NAME FRONTEND STATE, pinned on both lanes.
|
||||
//
|
||||
// Both backends used to carry their own arms for GL_SHADER_STORAGE_BUFFER_* and
|
||||
// GL_IMAGE_BINDING_* inside GLFunctionsTable::GetIntegeri_v, and their own
|
||||
// GetInteger64i_v / GetProgramiv table entries. None of it was reachable: GL_Getter and
|
||||
// GL_Program answer every one of these pnames from the frontend's own state and return
|
||||
// before the table is consulted. The duplicates did not even agree - the backend arms
|
||||
// clamped a bound range to the buffer's current storage, which GL 4.6 core tables
|
||||
// 23.4/23.5 do not permit - so the code was one refactor away from becoming the answer.
|
||||
// These cases pin what the frontend actually reports, so a future move of any of it back
|
||||
// behind the interface has to keep saying the same thing.
|
||||
TEST_F(AdvertisedLimitsScenario, IndexedBufferBindingsAreReportedVerbatimOnBothWidths) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
// A range that is NOT the whole buffer, so a clamp to the store would be visible.
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, 256, 512);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
GLint binding32 = -1;
|
||||
GLint start32 = -1;
|
||||
GLint size32 = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding32);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start32);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(binding32, static_cast<GLint>(buffer));
|
||||
EXPECT_EQ(start32, 256);
|
||||
EXPECT_EQ(size32, 512);
|
||||
|
||||
// The 64-bit width has to agree pname for pname. It has no backend entry of its own
|
||||
// and derives everything from the 32-bit answer above plus its own buffer arm.
|
||||
GLint64 binding64 = -1;
|
||||
GLint64 start64 = -1;
|
||||
GLint64 size64 = -1;
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding64);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start64);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size64);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(binding64, static_cast<GLint64>(buffer));
|
||||
EXPECT_EQ(start64, static_cast<GLint64>(256));
|
||||
EXPECT_EQ(size64, static_cast<GLint64>(512));
|
||||
|
||||
// An unbound index answers zero rather than erroring or leaking the driver's answer.
|
||||
GLint unbound = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 0, &unbound);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(unbound, 0);
|
||||
|
||||
// THE ARM THAT SEPARATES VERBATIM FROM CLAMPED. GL 4.6 core tables 23.4/23.5 report
|
||||
// the size glBindBufferRange was ASKED for; it does not follow the buffer, so
|
||||
// shrinking the store underneath the binding must not move it. A clamp to the
|
||||
// current storage - which is exactly what both backends' deleted arms did - answers
|
||||
// 128 here, and answers 0 for the bind-then-allocate shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-binding uses.
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint startAfterShrink = -1;
|
||||
GLint sizeAfterShrink = -1;
|
||||
GLint64 sizeAfterShrink64 = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &startAfterShrink);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink64);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(startAfterShrink, 256)
|
||||
<< "the bound range's start followed the buffer through a re-specification";
|
||||
EXPECT_EQ(sizeAfterShrink, 512)
|
||||
<< "the bound range's size was clamped to the buffer's current 128-byte storage; the range is "
|
||||
"state of the BINDING POINT and is reported verbatim";
|
||||
EXPECT_EQ(sizeAfterShrink64, static_cast<GLint64>(512))
|
||||
<< "the 64-bit width disagreed with the 32-bit one about the same pname";
|
||||
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
TEST_F(AdvertisedLimitsScenario, ImageUnitBindingsAreReportedFromTheFrontendState) {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
(void)FirstGLError();
|
||||
if (maxImageUnits < 2) GTEST_SKIP() << "no image units to bind on this lane";
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
glBindImageTexture(1, texture, 1, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
struct Expectation {
|
||||
GLenum pname;
|
||||
const char* name;
|
||||
GLint expected;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{GL_IMAGE_BINDING_NAME, "GL_IMAGE_BINDING_NAME", static_cast<GLint>(texture)},
|
||||
{GL_IMAGE_BINDING_LEVEL, "GL_IMAGE_BINDING_LEVEL", 1},
|
||||
{GL_IMAGE_BINDING_LAYERED, "GL_IMAGE_BINDING_LAYERED", GL_FALSE},
|
||||
{GL_IMAGE_BINDING_LAYER, "GL_IMAGE_BINDING_LAYER", 0},
|
||||
{GL_IMAGE_BINDING_ACCESS, "GL_IMAGE_BINDING_ACCESS", GL_READ_ONLY},
|
||||
{GL_IMAGE_BINDING_FORMAT, "GL_IMAGE_BINDING_FORMAT", GL_RGBA8},
|
||||
};
|
||||
for (const Expectation& expectation : expectations) {
|
||||
GLint value = -424242;
|
||||
glGetIntegeri_v(expectation.pname, 1, &value);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name;
|
||||
EXPECT_EQ(value, expectation.expected) << expectation.name;
|
||||
|
||||
// Same pname through the wide width - it must not fall through to a driver that
|
||||
// knows nothing about MobileGL's image-unit state.
|
||||
GLint64 wide = -424242;
|
||||
glGetInteger64i_v(expectation.pname, 1, &wide);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name << " (64-bit)";
|
||||
EXPECT_EQ(wide, static_cast<GLint64>(expectation.expected)) << expectation.name << " (64-bit)";
|
||||
}
|
||||
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glDeleteTextures(1, &texture);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
// glGetProgramiv(GL_COMPUTE_WORK_GROUP_SIZE) is a LINK ARTIFACT of the program the
|
||||
// application wrote. DirectVulkan used to answer it from its own spirv-reflect cache and
|
||||
// DirectGLES by forwarding to the driver's ESSL program - neither of which the
|
||||
// application ever named - while GL_Program.cpp has always answered it from
|
||||
// ProgramObject::GetComputeLocalSize. This pins the declared local size on both lanes.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeLocalSizeComesFromTheLinkedProgram) {
|
||||
static const char* kSource = R"(#version 430 core
|
||||
layout(local_size_x = 4, local_size_y = 3, local_size_z = 2) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
void main() { g_data[gl_LocalInvocationIndex] = 1u; }
|
||||
)";
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &kSource, 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);
|
||||
glDeleteShader(shader);
|
||||
(void)FirstGLError();
|
||||
GTEST_SKIP() << "no compute shader support on this lane: " << log;
|
||||
}
|
||||
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);
|
||||
glDeleteProgram(program);
|
||||
(void)FirstGLError();
|
||||
GTEST_SKIP() << "the compute program did not link on this lane: " << log;
|
||||
}
|
||||
|
||||
GLint localSize[3] = {-1, -1, -1};
|
||||
glGetProgramiv(program, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(localSize[0], 4);
|
||||
EXPECT_EQ(localSize[1], 3);
|
||||
EXPECT_EQ(localSize[2], 2);
|
||||
|
||||
// A program with no compute stage must answer INVALID_OPERATION, not a stale or
|
||||
// defaulted (1, 1, 1) - the frontend's rule, and the one a backend that answers from
|
||||
// its own reflection cache cannot express.
|
||||
const GLuint empty = glCreateProgram();
|
||||
GLint ignored[3] = {0, 0, 0};
|
||||
glGetProgramiv(empty, GL_COMPUTE_WORK_GROUP_SIZE, ignored);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION))
|
||||
<< "GL 4.6 core 7.13: the query is only defined for a linked program with a compute shader";
|
||||
|
||||
glDeleteProgram(empty);
|
||||
glDeleteProgram(program);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
// THE SIX COMPUTE LIMITS THAT OUTLIVE THE GETTER. GL_MAX_COMPUTE_WORK_GROUP_COUNT and
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, are the only indexed pnames the
|
||||
// DEVICE answers rather than the frontend (glGetIntegeri_v on Espryt, VkPhysicalDevice-
|
||||
// Limits on Magma), and therefore the only ones that have to cross the MGPipe boundary
|
||||
// once GetIntegeri_v is retired (plan B section 4.4.6 / P0.5). They ride in MGPCaps by
|
||||
// inclusion, as DynamicBackendParameters::MaxComputeWorkGroupCount/Size, filled by both
|
||||
// backends at capability init. This case pins that the caps copy and the live getter
|
||||
// answer are one number - the getter floors the backend's raw answer at the GL 4.3
|
||||
// minimum, so the comparison is against the floored caps value - and pins the
|
||||
// GL-visible half on every lane: answerability, the floors, vector/indexed agreement
|
||||
// and the index bound. On a lane where the caps block is out of reach (Android links
|
||||
// the shipping .so) only the GL-visible half runs.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeWorkGroupLimitsAreTheCapsBlocksAnswer) {
|
||||
struct Axis {
|
||||
GLenum pname;
|
||||
const char* name;
|
||||
GLint minimum[3]; // GL 4.3 core table 23.60
|
||||
};
|
||||
const Axis axes[] = {
|
||||
{GL_MAX_COMPUTE_WORK_GROUP_COUNT, "GL_MAX_COMPUTE_WORK_GROUP_COUNT", {65535, 65535, 65535}},
|
||||
{GL_MAX_COMPUTE_WORK_GROUP_SIZE, "GL_MAX_COMPUTE_WORK_GROUP_SIZE", {1024, 1024, 64}},
|
||||
};
|
||||
int capsCount[3] = {0, 0, 0};
|
||||
int capsSize[3] = {0, 0, 0};
|
||||
const bool capsVisible = PeekComputeWorkGroupCaps(capsCount, capsSize);
|
||||
|
||||
for (const Axis& axis : axes) {
|
||||
GLint indexed[3] = {-1, -1, -1};
|
||||
for (GLuint i = 0; i < 3; ++i) {
|
||||
glGetIntegeri_v(axis.pname, i, &indexed[i]);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name << "[" << i << "]";
|
||||
EXPECT_GE(indexed[i], axis.minimum[i])
|
||||
<< axis.name << "[" << i << "] = " << indexed[i]
|
||||
<< " is below the GL 4.3 core table 23.60 minimum " << axis.minimum[i];
|
||||
}
|
||||
GLint vector[3] = {-1, -1, -1};
|
||||
glGetIntegerv(axis.pname, vector);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(vector[i], indexed[i])
|
||||
<< axis.name << "[" << i << "]: the vector query and the indexed query disagree";
|
||||
}
|
||||
GLint outOfRange = -424242;
|
||||
glGetIntegeri_v(axis.pname, 3, &outOfRange);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_VALUE))
|
||||
<< axis.name << "[3]: an index past the three axes is INVALID_VALUE (GL 4.6 core 22.1)";
|
||||
|
||||
if (!capsVisible) continue;
|
||||
const int* capsAxis = axis.pname == GL_MAX_COMPUTE_WORK_GROUP_COUNT ? capsCount : capsSize;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(std::max(capsAxis[i], axis.minimum[i]), indexed[i])
|
||||
<< axis.name << "[" << i << "]: MGPCaps carries " << capsAxis[i]
|
||||
<< " but glGetIntegeri_v answers " << indexed[i]
|
||||
<< " - the caps block and the getter path must be one number, because P0.5 retires "
|
||||
"the getter in favour of the caps";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
// be able to turn this into a red.
|
||||
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
||||
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
|
||||
// (c) The extension string matches the configuration. This is the half a recorded
|
||||
// trace can never cover - Iris and Sodium change their submission schedule the
|
||||
// moment they see the string - so it is asserted against a real backend's real
|
||||
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
||||
// (c) The extension string matches the configuration - where "the configuration" is
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the
|
||||
// implementation says about itself. This is the half a recorded trace can never
|
||||
// cover - Iris and Sodium change their submission schedule the moment they see the
|
||||
// string - so it is asserted against a real backend's real GL_EXTENSIONS, through
|
||||
// both glGetString and glGetStringi.
|
||||
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||
// is synchronous. That is what the extension requires of a zero count.
|
||||
@@ -40,6 +42,27 @@
|
||||
//
|
||||
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||
// so this file runs twice per ctest invocation.
|
||||
//
|
||||
// COMPILATION MODE IS PER PROCESS TOO. Every case here needs a particular configuration of
|
||||
// MobileGL's shader compiler, and takes it from the ENVIRONMENT
|
||||
// (MOBILEGL_ASYNC_SHADER_COMPILE, MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS) rather than by
|
||||
// writing MG_Config::Features on the way past. Half of what those variables decide is
|
||||
// latched before the first GL call - the compile pool and its threads, and the advertised
|
||||
// extension list a backend builds once from the configuration in force at its first use -
|
||||
// so an in-process poke could only ever have moved the other half; and on Android it could
|
||||
// move nothing at all, because this module links against the shipping libMobileGL.so, which
|
||||
// exports no such symbol. A case whose process is not in the configuration it needs SKIPS
|
||||
// with that as its reason. CMakeLists.txt registers the extra ctest entries that put a
|
||||
// process into each configuration (AsyncOn., AsyncOff., OptimisticShaderStatus.), so one
|
||||
// ctest run still covers both sides of every switch. Run straight from a shell with nothing
|
||||
// set - the on-device shape - the ambient configuration runs and the rest skip cleanly.
|
||||
//
|
||||
// WITHIN one process, "compiled on a worker" versus "compiled on this thread" is switched
|
||||
// through glMaxShaderCompilerThreadsKHR, the extension's own entry point: a zero count joins
|
||||
// everything outstanding and compiles inline from then on, any nonzero count lifts that
|
||||
// again, and 0xFFFFFFFF asks for the implementation maximum (GL_Program.cpp,
|
||||
// MaxShaderCompilerThreadsKHR_State). Doing it through the public call rather than the
|
||||
// feature table means the switching is itself part of what these cases exercise.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -47,9 +70,6 @@
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
@@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
using MobileGL::MG_Config::QuirkOverride;
|
||||
|
||||
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
||||
// thing that can differ between the two compilation modes is the compilation.
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
@@ -139,50 +157,40 @@ void main() {
|
||||
return source;
|
||||
}
|
||||
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
||||
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
||||
// it in-process is what lets ONE ctest run compare the two modes against each
|
||||
// other - the whole point of (e).
|
||||
class AsyncModeScope {
|
||||
public:
|
||||
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
||||
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
||||
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
||||
// Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly
|
||||
// "MobileGL is configured to compile asynchronously" as an application can see it:
|
||||
// the backends gate the string on AsyncShaderCompileEnabled() and on nothing else
|
||||
// (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string
|
||||
// is the only way MobileGL ever tells anyone. A case that needs asynchronous
|
||||
// compilation checks for it the way an application would, and skips without it.
|
||||
//
|
||||
// The INDEXED form, because that is the one a core-profile application reads.
|
||||
bool HasParallelShaderCompile() {
|
||||
GLint count = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") return true;
|
||||
}
|
||||
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||
AsyncModeScope(const AsyncModeScope&) = delete;
|
||||
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
||||
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
||||
class OptimisticStatusScope {
|
||||
public:
|
||||
explicit OptimisticStatusScope(const QuirkOverride mode)
|
||||
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||
}
|
||||
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it
|
||||
// has to put the pool back or it changes how every scenario after it compiles.
|
||||
//
|
||||
// The restore is the extension's own "implementation maximum" spelling rather than a
|
||||
// hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined
|
||||
// (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State) as precisely the two steps this
|
||||
// used to perform through internal entry points - concurrency := the pool's full
|
||||
// thread count, then lift any suspension a zero count had armed - in the safer order,
|
||||
// since it raises the budget before re-admitting work rather than after. Going through
|
||||
// the public call also puts the restore path itself under test, and it is the only
|
||||
// spelling available on Android, where this module links the shipping shared library
|
||||
// and can reach nothing but the GL entry points.
|
||||
class CompilerThreadScope {
|
||||
public:
|
||||
CompilerThreadScope() = default;
|
||||
~CompilerThreadScope() {
|
||||
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
||||
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
||||
pool.SetMaxConcurrency(pool.GetThreadCount());
|
||||
}
|
||||
~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); }
|
||||
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||
};
|
||||
@@ -293,7 +301,12 @@ void main() {
|
||||
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so no compile can be "
|
||||
"outstanding; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
// One worker, so the queue behind it is what the poll observes.
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
@@ -341,14 +354,33 @@ void main() {
|
||||
}
|
||||
|
||||
// ---- (c) ------------------------------------------------------------------
|
||||
// The extension string, read from a real backend that really brought a driver
|
||||
// up. No mode forcing here: a backend builds its advertised list once, from the
|
||||
// configuration in force at its first use, so the meaningful assertion is
|
||||
// against the AMBIENT configuration - which is exactly what makes this case
|
||||
// worth running in both of the suite's flag states.
|
||||
// The extension string, read from a real backend that really brought a driver up.
|
||||
//
|
||||
// The expectation comes from the ENVIRONMENT, never from the implementation. This
|
||||
// case used to derive it by calling AsyncShaderCompileEnabled() - which is the same
|
||||
// function the backends gate the string on, so the two halves could only ever agree
|
||||
// and the case would have passed however wrong both of them were. Asserting an
|
||||
// implementation against itself pins nothing.
|
||||
//
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE is the whole input: the process inherited it before
|
||||
// any GL call, a backend builds its advertised list once from the configuration in
|
||||
// force at first use, and nothing in this process can move it afterwards. So reading
|
||||
// the variable IS reading the configuration, independently. With the variable unset
|
||||
// the configuration in force is MobileGL's built-in default, which only the
|
||||
// implementation knows - there is nothing independent left to compare against, and
|
||||
// this case says so rather than inventing an expectation. The AsyncOn. and AsyncOff.
|
||||
// ctest entries pin the variable to each of its two values, so one ctest run still
|
||||
// asserts both the advertised and the withdrawn side.
|
||||
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||
if (!Ready()) return;
|
||||
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
|
||||
const AmbientQuirk configured = AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
if (configured == AmbientQuirk::Auto) {
|
||||
GTEST_SKIP() << "MOBILEGL_ASYNC_SHADER_COMPILE is unset, so the configuration in force is "
|
||||
"MobileGL's built-in default and the only way to learn it would be to ask "
|
||||
"the implementation this case exists to check; the AsyncOn. and AsyncOff. "
|
||||
"ctest entries run it with the variable pinned to each of its two values";
|
||||
}
|
||||
const bool expected = configured == AmbientQuirk::On;
|
||||
|
||||
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
ASSERT_NE(extensions, nullptr);
|
||||
@@ -385,7 +417,12 @@ void main() {
|
||||
// A zero count must leave nothing in flight and keep it that way.
|
||||
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||
if (!Ready()) return;
|
||||
const AsyncModeScope async(true);
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so a zero count has "
|
||||
"nothing to settle; the AsyncOn. ctest entries run this case with "
|
||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
@@ -417,12 +454,28 @@ void main() {
|
||||
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||
// backend's orientation and present path live, so the comparison covers the
|
||||
// whole pipeline rather than the reflection tables alone.
|
||||
//
|
||||
// The two modes are selected through glMaxShaderCompilerThreadsKHR, the extension's
|
||||
// own entry point, rather than through the feature table: a zero count joins
|
||||
// everything outstanding and makes every later glCompileShader/glLinkProgram run its
|
||||
// body on the calling thread, and 0xFFFFFFFF lifts that again with the pool at its
|
||||
// full thread count (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State; the compile
|
||||
// and link paths both gate on AsyncShaderCompileActive(), which is what the zero
|
||||
// count switches). So this is still one process comparing worker-built artifacts
|
||||
// against inline-built ones - just asked for the way an application asks.
|
||||
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so both halves would "
|
||||
"be the same inline build and the comparison would be vacuous; the "
|
||||
"AsyncOn. ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
Image asyncImage;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
asyncImage = DrawFrameWith(program);
|
||||
@@ -431,7 +484,7 @@ void main() {
|
||||
|
||||
Image syncImage;
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
glMaxShaderCompilerThreadsKHR(0);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u);
|
||||
syncImage = DrawFrameWith(program);
|
||||
@@ -456,11 +509,16 @@ void main() {
|
||||
// candidate) shows up here and not in the single-program case above.
|
||||
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||
if (!Ready()) return;
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "this process is configured to compile inline "
|
||||
"(GL_KHR_parallel_shader_compile is not advertised), so nothing would be "
|
||||
"built on a worker and there is no per-worker state to leak; the AsyncOn. "
|
||||
"ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
// Everything enqueued before anything is read: the only shape in which
|
||||
@@ -489,6 +547,21 @@ void main() {
|
||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||
if (!Ready()) return;
|
||||
// The quirk is off by default and never advertised, so unlike the cases above
|
||||
// there is no GL observable that says whether it is in force - only the variable
|
||||
// that put it there. It also has to be set BEFORE this process started for the
|
||||
// shape to be the real one: the optimistic answer is latched per compile, and a
|
||||
// quirk switched on mid-process would only cover the compiles after it.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case is the optimistic-status quirk's end-to-end shape and needs it on "
|
||||
"for the whole process; the OptimisticShaderStatus. ctest entries run it with "
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1";
|
||||
}
|
||||
if (!HasParallelShaderCompile()) {
|
||||
GTEST_SKIP() << "the optimistic status only ever applies to a compile that is still in flight "
|
||||
"(OptimisticShaderStatusActive() requires AsyncShaderCompileActive()), and "
|
||||
"this process is configured to compile inline";
|
||||
}
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||
@@ -508,8 +581,6 @@ void main() {
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
|
||||
@@ -87,11 +87,6 @@ void main() {
|
||||
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
|
||||
<< "; this needs 3 and 2";
|
||||
}
|
||||
if (!AtomicCountersAreWired()) {
|
||||
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
|
||||
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
|
||||
<< "is still resolved from the shader-storage binding points";
|
||||
}
|
||||
m_program = CompileComputeProgram(kCounterComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
}
|
||||
@@ -105,12 +100,6 @@ void main() {
|
||||
m_program = 0;
|
||||
}
|
||||
|
||||
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
|
||||
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
|
||||
// frontend half (limits, reflection queries, the link-time offset rules) is
|
||||
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
|
||||
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
@@ -236,4 +225,43 @@ void main() {
|
||||
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
||||
}
|
||||
|
||||
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
||||
// between. Each backend has its own way to invert this pair, and both are pinned here.
|
||||
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
|
||||
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
|
||||
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
|
||||
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
|
||||
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
|
||||
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
|
||||
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
|
||||
// the deferred frame command buffer. Unless the frontend retires that pending work before
|
||||
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
|
||||
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
|
||||
// pins the other direction for both: the upload must leave bytes outside its range - the
|
||||
// dispatch's results - untouched.
|
||||
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||
MakeCounterBuffer(1, {0u});
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
Dispatch();
|
||||
|
||||
const unsigned int reseed = 4242u;
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
|
||||
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), &reseed);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
|
||||
|
||||
const std::vector<unsigned int> values = ReadCounters(zero, 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(values[0], reseed)
|
||||
<< "offset 0 read back " << values[0] << "; the dispatch's value (" << kInvocations
|
||||
<< ") means the readback ran before the queued SubData range was flushed and reverted it";
|
||||
EXPECT_EQ(values[1], 2 * kInvocations)
|
||||
<< "offset 4 read back " << values[1] << "; the SubData flush must leave bytes outside its "
|
||||
<< "range untouched";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED.
|
||||
//
|
||||
// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE
|
||||
// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level
|
||||
// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with
|
||||
// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no
|
||||
// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body
|
||||
// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and
|
||||
// all three entry points.
|
||||
//
|
||||
// The frontend understands this shape - the clear is a pure CPU-shadow write, and
|
||||
// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The
|
||||
// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a
|
||||
// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and
|
||||
// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly
|
||||
// as it found it. The conformance failures carried no <Text> at all, because nothing raised a GL
|
||||
// error: the destination was simply never written, so the test compared its own zero-initialized
|
||||
// buffer against the clear value.
|
||||
//
|
||||
// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have
|
||||
// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on
|
||||
// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would
|
||||
// return stale bytes for every render-to-texture result instead.
|
||||
//
|
||||
// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible
|
||||
// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a
|
||||
// separate change (it moves every GL-level-to-subresource translation in the backend); this
|
||||
// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able
|
||||
// to define a lone level N - is the built-in control for what the answer should be.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The conformance family's own shape: a mid-chain level of a texture that has nothing else.
|
||||
constexpr GLint kDefinedLevel = 3;
|
||||
constexpr GLsizei kLevelExtent = 8;
|
||||
|
||||
struct Texel8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
bool operator==(const Texel8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Texel8& c) {
|
||||
return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||
}
|
||||
|
||||
// The conformance test's clear value is a single repeated component; 5 is what it uses, and
|
||||
// it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one).
|
||||
constexpr Texel8 kClearValue{5, 5, 5, 5};
|
||||
constexpr Texel8 kInitialValue{200, 100, 50, 255};
|
||||
|
||||
class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_texture != 0) {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, &m_texture);
|
||||
m_texture = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D,
|
||||
// which would define the whole chain and could not express "level 0 does not exist".
|
||||
void MakeTextureWithOnlyLevel(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> initial(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
initial.data());
|
||||
// What the conformance case does: MAX_LEVEL names the one level that exists, and
|
||||
// BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND
|
||||
// nominally the base level, the shape the backend could not express.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level;
|
||||
}
|
||||
|
||||
std::vector<Texel8> ReadLevel(GLint level) {
|
||||
std::vector<Texel8> pixels(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0});
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind";
|
||||
return pixels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, const std::vector<Texel8>& pixels, Texel8 expected) {
|
||||
std::size_t offenders = 0;
|
||||
Texel8 firstBad{};
|
||||
for (const Texel8& pixel : pixels) {
|
||||
if (pixel == expected) continue;
|
||||
if (offenders == 0) firstBad = pixel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " ("
|
||||
<< offenders << " of " << pixels.size() << " texels wrong)";
|
||||
}
|
||||
|
||||
// Level 0 defined, a GAP, then `level` defined. GL keeps the intervening levels at a zero
|
||||
// extent, so the backend's mip walk stops at the gap and the VkImage ends up with FEWER
|
||||
// mip levels than the GL level count - which is a different shape from "no image at all"
|
||||
// and is why the readback has to bound the level against the IMAGE.
|
||||
void MakeTextureWithAGapBefore(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> base(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
base.data());
|
||||
const std::vector<Texel8> gapped(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, gapped.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with a gap before level " << level;
|
||||
}
|
||||
|
||||
GLuint m_texture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the
|
||||
// caller's buffer kept whatever it already held - which is why the conformance failures showed
|
||||
// the test's own zero-initialized memory and carried no GL error.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
// Pre-flight: the level reads back as what was uploaded. This is what makes the assertion
|
||||
// after the clear falsifiable - without it, a readback that silently wrote nothing could not
|
||||
// be told from one that wrote the right answer.
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same shape through glClearTexSubImage, which is a separate entry point in the conformance
|
||||
// family and failed on exactly the same bodies.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from
|
||||
// the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally
|
||||
// would pass the two tests above and this one too - but it would also hand back stale bytes for
|
||||
// anything the GPU had written, which is why the partial-clear check below matters: the readback
|
||||
// has to see a region the backend cleared and a region it did not, in one image.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(0);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue);
|
||||
|
||||
// Clear only the left half, so the answer is neither "all initial" nor "all cleared".
|
||||
glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
&kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
const std::vector<Texel8> pixels = ReadLevel(0);
|
||||
ASSERT_EQ(pixels.size(), static_cast<std::size_t>(kLevelExtent) * kLevelExtent);
|
||||
for (int y = 0; y < kLevelExtent; ++y) {
|
||||
for (int x = 0; x < kLevelExtent; ++x) {
|
||||
const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue;
|
||||
const Texel8 actual = pixels[static_cast<std::size_t>(y) * kLevelExtent + x];
|
||||
ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")";
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The adjacent shape the first fix did NOT cover: level 0 defined, a gap, then the level being
|
||||
// read. This one DOES get a VkImage - just one with fewer mip levels than GL thinks the texture
|
||||
// has - so the "no VkImage" test passes and the GL level was written straight into
|
||||
// imageSubresource.mipLevel and into a VkImageMemoryBarrier's baseMipLevel. An out-of-range
|
||||
// subresource is a promise the driver takes at face value; the glCopyImageSubData path two
|
||||
// functions away grew the same guard after it SIGSEGV'd inside the Adreno driver.
|
||||
//
|
||||
// The level being read really does hold its own data (the shadow is its only copy, since nothing
|
||||
// ever uploaded it), so the correct answer is the uploaded bytes - not a decline.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadBackALevelSeparatedFromLevelZeroByAGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithAGapBefore(kDefinedLevel);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
|
||||
// Level 0 is backed by the real image and must still read back from it, so the level bound is
|
||||
// about the level and not about the texture.
|
||||
ExpectAllTexels("level 0 after clearing level 3", ReadLevel(0), kInitialValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,375 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
|
||||
//
|
||||
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
|
||||
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
|
||||
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
|
||||
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
|
||||
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
|
||||
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
|
||||
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
|
||||
// texel-block move - between a mirrored allocation and a plain one lands the fields
|
||||
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
|
||||
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
|
||||
// image are clean (the driver decodes its own layout consistently), which is why only the
|
||||
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
|
||||
// checks always passed.
|
||||
//
|
||||
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
|
||||
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
|
||||
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
|
||||
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
|
||||
// the driver's layout.
|
||||
//
|
||||
// The repair under test is the packed16 storage widening
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
|
||||
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
|
||||
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
|
||||
// encodes to UNorm8 and back losslessly for every n <= 8.
|
||||
//
|
||||
// This scenario runs in BOTH configurations, and both must hand back identical client words:
|
||||
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
|
||||
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
|
||||
// covered);
|
||||
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
|
||||
// which is the storage every affected device will actually run - without it the repair
|
||||
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
|
||||
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
|
||||
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
|
||||
// client-invisible: same words in, same words out, on every leg the failing bodies used.
|
||||
//
|
||||
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
|
||||
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
|
||||
// means the widening (or the narrow path it replaces) is.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kBaseSize = 30; // array level 0; level 1 is 15x15
|
||||
constexpr int kLevel1Size = kBaseSize / 2;
|
||||
constexpr int kLayers = 12;
|
||||
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
|
||||
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
|
||||
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
|
||||
constexpr int kRegion = kFlatSize;
|
||||
constexpr int kArrayOffset = 8;
|
||||
|
||||
struct PackedFormatCase {
|
||||
GLenum internalFormat; // the spelling the CTS uses
|
||||
GLenum transferFormat;
|
||||
GLenum transferType;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
// Per-texel varying words, every field inside its width, so a swapped field order (or
|
||||
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
|
||||
GLushort MakeWord(GLenum type, int i) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
|
||||
return static_cast<GLushort>((r << 11) | (g << 5) | b);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4: {
|
||||
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
|
||||
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
|
||||
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
|
||||
std::vector<GLushort> words(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
class CopyImagePacked16Scenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
|
||||
// every odd-width row of the 15x15 level and shear the comparisons.
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 2);
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
|
||||
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
|
||||
// below can pass by reading a level that was never written.
|
||||
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
|
||||
const std::vector<GLushort>& level1) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
|
||||
kLayers, 0, format.transferFormat, format.transferType, level0.data());
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
|
||||
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
|
||||
const int level2Size = kLevel1Size / 2;
|
||||
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
|
||||
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
|
||||
// every assertion reads.
|
||||
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
|
||||
format.transferFormat, format.transferType, texels.data());
|
||||
for (int level = 1; level <= 2; ++level) {
|
||||
const int size = std::max(kFlatSize >> level, 1);
|
||||
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
|
||||
format.transferFormat, format.transferType, fill.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
|
||||
const PackedFormatCase& format, size_t texelCount) {
|
||||
std::vector<GLushort> words(texelCount, 0);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
|
||||
glBindTexture(target, 0);
|
||||
return words;
|
||||
}
|
||||
|
||||
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
|
||||
// image equals the corresponding source word, and every word outside it still holds
|
||||
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
|
||||
// regression into a one-line diagnosis.
|
||||
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
|
||||
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
|
||||
const std::vector<GLushort>& fill, const char* what) {
|
||||
for (int y = 0; y < width; ++y) {
|
||||
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
|
||||
const bool inRegion =
|
||||
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
|
||||
const GLushort actual = got[static_cast<size_t>(y * width + x)];
|
||||
const GLushort expected =
|
||||
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
|
||||
(x - x0))]
|
||||
: fill[static_cast<size_t>(y * width + x)];
|
||||
EXPECT_EQ(actual, expected)
|
||||
<< what << ": texel (" << x << ", " << y << ")"
|
||||
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
|
||||
<< ", expected 0x" << expected;
|
||||
if (actual != expected) return; // one texel names the defect; 224 more would bury it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
const PackedFormatCase kFormats[] = {
|
||||
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
|
||||
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
|
||||
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
|
||||
};
|
||||
|
||||
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
|
||||
// the array-as-destination direction of 12 of the 18 failing bodies.
|
||||
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint source = MakeFlatTexture(format, flat);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
|
||||
kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
|
||||
(std::string("2d->2d_array level 1, ") + format.name).c_str());
|
||||
// The source must not have moved - the CTS asserts this before it ever looks at
|
||||
// the destination, and it is what pins the corruption to the copy itself.
|
||||
const auto sourceAfter =
|
||||
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
|
||||
(std::string("source after 2d->2d_array, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
|
||||
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
|
||||
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint destination = MakeFlatTexture(format, fill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
|
||||
static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
|
||||
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
|
||||
// that requires the renderbuffer's ES storage to move together with the textures' -
|
||||
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
|
||||
// reached textures alone would break exactly here.
|
||||
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
|
||||
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< format.name << ": the renderbuffer is not attachable";
|
||||
// Field values picked to encode exactly in the narrow fields AND in their
|
||||
// UNorm8 expansions, so the expected word is the same whichever storage the
|
||||
// configuration picked - which is the point of the whole scenario.
|
||||
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
|
||||
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
|
||||
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
|
||||
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
|
||||
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
|
||||
static_cast<GLfloat>(max - 2) / max, 1.0f};
|
||||
// The context is shared with every scenario in this process; a scissor left on
|
||||
// would clip the clear and hand the copy undefined renderbuffer texels.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearBufferfv(GL_COLOR, 0, clearColor);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
|
||||
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
GLushort clearedWord = 0;
|
||||
switch (format.transferType) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4:
|
||||
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
|
||||
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,238 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
|
||||
//
|
||||
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
|
||||
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
|
||||
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
|
||||
// the capability IS there both backends translate the factors properly, and that has always
|
||||
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
|
||||
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
|
||||
// application asking for a blend factor the device cannot do is a picture problem, never a reason
|
||||
// to take the process down.
|
||||
//
|
||||
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
|
||||
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
|
||||
// scenario pins that it lands on one of them and never on a crash:
|
||||
//
|
||||
// capability present - src0 * src1 + dst * (1 - src1)
|
||||
// capability absent - src0, written straight through
|
||||
//
|
||||
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
|
||||
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
|
||||
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
|
||||
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
|
||||
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
|
||||
// scenario exists for - is unit-tested against stubbed capabilities in
|
||||
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
|
||||
// which is the only place it can be reached without a driver that lacks the extension.
|
||||
//
|
||||
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
|
||||
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
|
||||
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
|
||||
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
|
||||
// would still be invalid pipeline state.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
|
||||
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
|
||||
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
|
||||
// harness's compile-and-link helper.
|
||||
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uSrc0;
|
||||
uniform vec4 uSrc1;
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main()
|
||||
{
|
||||
fragColor0 = uSrc0;
|
||||
fragColor1 = uSrc1;
|
||||
}
|
||||
)";
|
||||
|
||||
class DualSourceBlendScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
|
||||
m_programError = error;
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// Capability probe, not an assertion. A GL link that succeeded is not proof that
|
||||
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
|
||||
// use, and GLSL ES has no `index` layout qualifier outside
|
||||
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
|
||||
// and the draw renders nothing. One unblended white draw tells the two apart, and
|
||||
// the cases skip rather than measure a picture the driver never produced.
|
||||
if (m_program != 0) {
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
|
||||
glFinish();
|
||||
const Image probe = ReadPixels(kExtent, kExtent);
|
||||
m_programRenders =
|
||||
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
|
||||
}
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
}
|
||||
|
||||
void Draw(float src0, float src1) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
// Both cases share this gate: nothing below can be measured on a backend that cannot
|
||||
// run a dual-source fragment program at all. Returns the skip reason, empty when the
|
||||
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
|
||||
// expands to a `return` and would leave only the HELPER, letting the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyTheProgramCannotRun() const {
|
||||
if (m_program == 0) {
|
||||
return "this driver cannot build a dual-source fragment shader: " + m_programError;
|
||||
}
|
||||
if (!m_programRenders) {
|
||||
return "this backend links a dual-source fragment program but renders nothing with it "
|
||||
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
bool m_programRenders = false;
|
||||
std::string m_programError;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The whole point of the scenario: this sequence used to be a process kill on any device
|
||||
// without the capability, and it has to be a picture either way.
|
||||
//
|
||||
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
|
||||
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
|
||||
// declined = 1.0 -> 255
|
||||
// Anything else means the factors were mistranslated rather than either honoured or declined.
|
||||
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
|
||||
glFinish();
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
const int red = static_cast<int>(centre.r);
|
||||
const bool blended = red > 100 && red < 160;
|
||||
const bool declined = red > 245;
|
||||
EXPECT_TRUE(blended || declined)
|
||||
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
|
||||
<< "straight-through source (255) - the SRC1 factors were mistranslated";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
|
||||
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
|
||||
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
|
||||
// source either way, so this case is really "no crash, no error, no surprise".
|
||||
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(static_cast<int>(centre.r), 245)
|
||||
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,794 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GuiBatchScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A GUI QUAD DRAWN THE WAY AcceleratedRendering DRAWS ONE.
|
||||
//
|
||||
// The mod replaces every GUI blit with a compute pass: the application writes unit-space
|
||||
// vertices into a persistently mapped SSBO, a compute shader multiplies them by a shared
|
||||
// transform into a second buffer, and that second buffer is then bound as GL_ARRAY_BUFFER of a
|
||||
// DSA vertex array and drawn with glDrawElementsBaseVertex through vanilla's position_tex_color
|
||||
// program. Every element of that is replayed here, one axis per test, so a failure names the
|
||||
// element that killed the quad rather than "the GUI is broken".
|
||||
//
|
||||
// The axis that mattered is MeshesBlockLeftUnbound. The mod's vertex-transform compute shader
|
||||
// declares SIX storage blocks, and the last of them - `Meshes`, the cache of pre-uploaded model
|
||||
// geometry - is read only when a vertex says it comes from a cached mesh. A batch of plain GUI
|
||||
// blits has no cached meshes, so the mod binds nothing at that point and the shader never reads
|
||||
// it. GL 4.6 core 7.8 is explicit that this is legal: a storage block with no buffer at its
|
||||
// binding point simply has no store.
|
||||
//
|
||||
// DirectVulkan used to refuse the whole descriptor set over it, and both SetupDraw and
|
||||
// DispatchCompute skip their work on that refusal - so the transform dispatch never ran, the
|
||||
// output vertex buffer kept whatever was in it, and every hotbar and container-screen background
|
||||
// quad came out degenerate. Items were unaffected because item geometry DOES come from cached
|
||||
// meshes, which is what made the bug look like "only the backgrounds disappear".
|
||||
//
|
||||
// The assertions are whole-region, not centre-pixel: a quad that survives with three stale
|
||||
// vertices still paints its centre.
|
||||
//
|
||||
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound binding to the GLES driver,
|
||||
// which does what GL says, so it is the control - every test here must stay green on both.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTransformComputeSource = R"(#version 460 core
|
||||
|
||||
struct Vertex {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
float u0;
|
||||
float v0;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct VaryingData {
|
||||
int offset;
|
||||
int sharing;
|
||||
int mesh;
|
||||
int shouldCull;
|
||||
};
|
||||
|
||||
struct SharingData {
|
||||
mat4 transform;
|
||||
mat3 normal;
|
||||
};
|
||||
|
||||
layout(local_size_x = 128) in;
|
||||
|
||||
layout(binding=0, std430) restrict readonly buffer VerticesIn {
|
||||
Vertex verticesIn[];
|
||||
};
|
||||
|
||||
layout(binding=1, std430) restrict writeonly buffer VerticesOut {
|
||||
Vertex verticesOut[];
|
||||
};
|
||||
|
||||
layout(binding=2, std430) restrict readonly buffer Sharings {
|
||||
SharingData sharings[];
|
||||
};
|
||||
|
||||
layout(binding=3, std430) restrict readonly buffer VaryingsIn {
|
||||
VaryingData varyingsIn[];
|
||||
};
|
||||
|
||||
layout(binding=4, std430) restrict writeonly buffer VaryingsOut {
|
||||
VaryingData varyingsOut[];
|
||||
};
|
||||
|
||||
layout(binding=5, std430) restrict readonly buffer Meshes {
|
||||
Vertex meshVertices[];
|
||||
};
|
||||
|
||||
layout(location=0) uniform uint vertexCount;
|
||||
layout(location=1) uniform uint vertexOffset;
|
||||
layout(location=2) uniform uint varyingOffset;
|
||||
|
||||
void main() {
|
||||
uint indexIn = gl_GlobalInvocationID.x;
|
||||
uint vertexOut = indexIn + vertexOffset;
|
||||
uint varyingOut = indexIn + varyingOffset;
|
||||
|
||||
if (indexIn >= vertexCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
int offset = varyingsIn[indexIn] .offset;
|
||||
uint reference = indexIn - offset;
|
||||
int sharing = varyingsIn[reference] .sharing;
|
||||
int mesh = varyingsIn[reference] .mesh;
|
||||
|
||||
mat4 transformMatrix;
|
||||
|
||||
if (sharing != -1) {
|
||||
transformMatrix = sharings[sharing].transform;
|
||||
} else {
|
||||
transformMatrix = mat4(1.0);
|
||||
}
|
||||
|
||||
Vertex vertexIn;
|
||||
vec4 colorMesh;
|
||||
|
||||
if (mesh != -1) {
|
||||
vertexIn = meshVertices[mesh + offset];
|
||||
colorMesh = unpackUnorm4x8 (vertexIn.color);
|
||||
} else {
|
||||
vertexIn = verticesIn[indexIn];
|
||||
colorMesh = vec4 (1.0);
|
||||
}
|
||||
|
||||
vec4 colorIn = unpackUnorm4x8 (verticesIn[reference].color);
|
||||
|
||||
vec4 posOut = transformMatrix * vec4 (vertexIn.x, vertexIn.y, vertexIn.z, 1.0);
|
||||
vec4 colorOut = colorMesh * colorIn;
|
||||
|
||||
verticesOut[vertexOut].x = posOut.x;
|
||||
verticesOut[vertexOut].y = posOut.y;
|
||||
verticesOut[vertexOut].z = posOut.z;
|
||||
|
||||
verticesOut[vertexOut].u0 = vertexIn.u0;
|
||||
verticesOut[vertexOut].v0 = vertexIn.v0;
|
||||
|
||||
verticesOut[vertexOut].color = packUnorm4x8 (colorOut);
|
||||
|
||||
varyingsOut[varyingOut].offset = offset;
|
||||
varyingsOut[varyingOut].shouldCull = varyingsIn[reference].shouldCull;
|
||||
}
|
||||
)";
|
||||
|
||||
// Vanilla position_tex_color, spelled the way MC ships it: #version 150, no explicit
|
||||
// attribute locations (they come from glBindAttribLocation in format order) and the
|
||||
// ProjMat/ModelViewMat pair the mod re-uploads through setDefaultUniforms.
|
||||
constexpr const char* kBlitVertexSource = R"(#version 150
|
||||
in vec3 Position;
|
||||
in vec2 UV0;
|
||||
in vec4 Color;
|
||||
uniform mat4 ModelViewMat;
|
||||
uniform mat4 ProjMat;
|
||||
out vec2 texCoord0;
|
||||
out vec4 vertexColor;
|
||||
void main() {
|
||||
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
|
||||
texCoord0 = UV0;
|
||||
vertexColor = Color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kBlitFragmentSource = R"(#version 150
|
||||
uniform sampler2D Sampler0;
|
||||
in vec2 texCoord0;
|
||||
in vec4 vertexColor;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(Sampler0, texCoord0) * vertexColor;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kFboSize = 64;
|
||||
constexpr int kShaderStorageRestoreRange = 9;
|
||||
constexpr int kAtomicCounterRestoreRange = 1;
|
||||
constexpr int kBuilders = 2;
|
||||
|
||||
struct GuiVertex {
|
||||
float x, y, z;
|
||||
float u, v;
|
||||
std::uint32_t color;
|
||||
};
|
||||
static_assert(sizeof(GuiVertex) == 24, "POSITION_TEX_COLOR is 24 bytes");
|
||||
|
||||
struct VaryingData {
|
||||
std::int32_t offset;
|
||||
std::int32_t sharing;
|
||||
std::int32_t mesh;
|
||||
std::int32_t shouldCull;
|
||||
};
|
||||
|
||||
struct IndexedBinding {
|
||||
GLint buffer = 0;
|
||||
GLint start = 0;
|
||||
GLint size = 0;
|
||||
};
|
||||
|
||||
// Which parts of the mod's real frame this replay reproduces. Each test flips exactly
|
||||
// one on top of the baseline so a failure names the element that killed the quad.
|
||||
struct Fidelity {
|
||||
bool shortIndices = false; // MC's AutoStorageIndexBuffer is USHORT at small counts
|
||||
bool baseVertex = false; // ...and the second builder draws at a base vertex
|
||||
bool twoBuilders = false; // two render types share one output buffer
|
||||
bool blendAndDepth = false; // TRANSLUCENT_TRANSPARENCY + LEQUAL_DEPTH_TEST
|
||||
bool regrow = false; // MutableBuffer.doExpand replaces the GL name
|
||||
bool rewriteMapEachFrame = false;
|
||||
bool skipRelayout = false; // bindDrawBuffers() only re-lays-out when resized
|
||||
bool leaveMeshesUnbound = false; // a batch with no server meshes never binds binding 5
|
||||
};
|
||||
|
||||
class GuiBatchScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_transform = CompileComputeProgram(kTransformComputeSource);
|
||||
ASSERT_NE(m_transform, 0u) << m_buildLog;
|
||||
m_blit = CompileBlitProgram();
|
||||
ASSERT_NE(m_blit, 0u) << m_buildLog;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u);
|
||||
|
||||
MakeTexture();
|
||||
MakeIndexBuffers();
|
||||
MakeAcceleratedBuffers();
|
||||
m_laidOut = false;
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
DestroyColorFbo(m_target);
|
||||
if (m_transform) glDeleteProgram(m_transform);
|
||||
if (m_blit) glDeleteProgram(m_blit);
|
||||
if (m_texture) glDeleteTextures(1, &m_texture);
|
||||
if (!m_buffers.empty()) glDeleteBuffers((GLsizei)m_buffers.size(), m_buffers.data());
|
||||
if (!m_vaos.empty()) glDeleteVertexArrays((GLsizei)m_vaos.size(), m_vaos.data());
|
||||
m_buffers.clear();
|
||||
m_vaos.clear();
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileOne(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// glBindAttribLocation in format order, exactly as ShaderInstance does it.
|
||||
unsigned int CompileBlitProgram() {
|
||||
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kBlitVertexSource);
|
||||
if (!vs) return 0;
|
||||
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kBlitFragmentSource);
|
||||
if (!fs) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glBindAttribLocation(program, 0, "Position");
|
||||
glBindAttribLocation(program, 1, "UV0");
|
||||
glBindAttribLocation(program, 2, "Color");
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("blit program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
void MakeTexture() {
|
||||
std::vector<std::uint8_t> pixels(4 * 4 * 4, 0);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
pixels[i * 4 + 2] = 255;
|
||||
pixels[i * 4 + 3] = 255;
|
||||
}
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
}
|
||||
|
||||
GLuint NewBuffer() {
|
||||
GLuint b = 0;
|
||||
glCreateBuffers(1, &b);
|
||||
m_buffers.push_back(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
GLuint NewVao() {
|
||||
GLuint v = 0;
|
||||
glCreateVertexArrays(1, &v);
|
||||
m_vaos.push_back(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
void MakeIndexBuffers() {
|
||||
std::uint32_t wide[12];
|
||||
std::uint16_t narrow[12];
|
||||
for (int quad = 0; quad < 2; ++quad) {
|
||||
const std::uint32_t base = (std::uint32_t)(quad * 4);
|
||||
const std::uint32_t pattern[6] = {base, base + 1, base + 2, base + 2, base + 3, base};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
wide[quad * 6 + i] = pattern[i];
|
||||
narrow[quad * 6 + i] = (std::uint16_t)pattern[i];
|
||||
}
|
||||
}
|
||||
m_wideIndices = NewBuffer();
|
||||
glNamedBufferStorage(m_wideIndices, sizeof(wide), wide, 0);
|
||||
m_narrowIndices = NewBuffer();
|
||||
glNamedBufferStorage(m_narrowIndices, sizeof(narrow), narrow, 0);
|
||||
}
|
||||
|
||||
static void SetupAttributes() {
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (const void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 24, (const void*)12);
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 24, (const void*)20);
|
||||
glEnableVertexAttribArray(2);
|
||||
}
|
||||
|
||||
void WriteInputs() {
|
||||
const GuiVertex unit[4] = {
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0xFFFFFFFFu},
|
||||
{0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0xFFFFFFFFu},
|
||||
{1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0xFFFFFFFFu},
|
||||
{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0xFFFFFFFFu},
|
||||
};
|
||||
for (int b = 0; b < kBuilders; ++b) {
|
||||
std::memcpy(m_inVertexMap[b], unit, sizeof(unit));
|
||||
VaryingData varyings[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
varyings[i].offset = i;
|
||||
varyings[i].sharing = b;
|
||||
varyings[i].mesh = -1;
|
||||
varyings[i].shouldCull = 0;
|
||||
}
|
||||
std::memcpy(m_inVaryingMap[b], varyings, sizeof(varyings));
|
||||
}
|
||||
}
|
||||
|
||||
void MakeAcceleratedBuffers() {
|
||||
const GLbitfield persistent = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
|
||||
for (int b = 0; b < kBuilders; ++b) {
|
||||
m_inVertices[b] = NewBuffer();
|
||||
glNamedBufferStorage(m_inVertices[b], 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, persistent);
|
||||
m_inVertexMap[b] =
|
||||
glMapNamedBufferRange(m_inVertices[b], 0, 4 * (GLsizeiptr)sizeof(GuiVertex), persistent);
|
||||
m_inVaryings[b] = NewBuffer();
|
||||
glNamedBufferStorage(m_inVaryings[b], 4 * (GLsizeiptr)sizeof(VaryingData), nullptr, persistent);
|
||||
m_inVaryingMap[b] =
|
||||
glMapNamedBufferRange(m_inVaryings[b], 0, 4 * (GLsizeiptr)sizeof(VaryingData), persistent);
|
||||
}
|
||||
|
||||
// Two SharingData entries: builder 0 lands left of centre, builder 1 right.
|
||||
float sharing[56] = {};
|
||||
const float tx[2] = {-0.9f, 0.1f};
|
||||
for (int b = 0; b < 2; ++b) {
|
||||
float* m = sharing + b * 28;
|
||||
m[0] = 0.8f;
|
||||
m[5] = 1.0f;
|
||||
m[10] = 1.0f;
|
||||
m[15] = 1.0f;
|
||||
m[12] = tx[b];
|
||||
m[13] = -0.5f;
|
||||
m[16] = 1.0f;
|
||||
m[20] = 1.0f;
|
||||
m[24] = 1.0f;
|
||||
}
|
||||
m_sharings = NewBuffer();
|
||||
glNamedBufferStorage(m_sharings, sizeof(sharing), nullptr, persistent);
|
||||
void* r = glMapNamedBufferRange(m_sharings, 0, sizeof(sharing), persistent);
|
||||
std::memcpy(r, sharing, sizeof(sharing));
|
||||
|
||||
m_outSize = 8 * (GLsizeiptr)sizeof(GuiVertex);
|
||||
m_outVertices = NewBuffer();
|
||||
glNamedBufferStorage(m_outVertices, m_outSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
m_outVaryings = NewBuffer();
|
||||
glNamedBufferStorage(m_outVaryings, 8 * (GLsizeiptr)sizeof(VaryingData), nullptr,
|
||||
GL_DYNAMIC_STORAGE_BIT);
|
||||
m_meshes = NewBuffer();
|
||||
glNamedBufferStorage(m_meshes, 4 * (GLsizeiptr)sizeof(GuiVertex), nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
|
||||
m_vao = NewVao();
|
||||
WriteInputs();
|
||||
}
|
||||
|
||||
std::vector<IndexedBinding> Record(GLenum b, GLenum s, GLenum z, int range) {
|
||||
std::vector<IndexedBinding> saved((std::size_t)range);
|
||||
for (int i = 0; i < range; ++i) {
|
||||
glGetIntegeri_v(b, (GLuint)i, &saved[(std::size_t)i].buffer);
|
||||
glGetIntegeri_v(s, (GLuint)i, &saved[(std::size_t)i].start);
|
||||
glGetIntegeri_v(z, (GLuint)i, &saved[(std::size_t)i].size);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
void Restore(GLenum target, const std::vector<IndexedBinding>& saved) {
|
||||
for (std::size_t i = 0; i < saved.size(); ++i) {
|
||||
if (saved[i].start == 0 && saved[i].size == 0) {
|
||||
glBindBufferBase(target, (GLuint)i, (GLuint)saved[i].buffer);
|
||||
} else {
|
||||
glBindBufferRange(target, (GLuint)i, (GLuint)saved[i].buffer, saved[i].start,
|
||||
saved[i].size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MutableBuffer.doExpand: a NEW immutable store, copied from the old one, old one gone.
|
||||
void RegrowOutputBuffer() {
|
||||
const GLsizeiptr newSize = m_outSize * 2;
|
||||
GLuint grown = 0;
|
||||
glCreateBuffers(1, &grown);
|
||||
glNamedBufferStorage(grown, newSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
|
||||
glCopyNamedBufferSubData(m_outVertices, grown, 0, 0, m_outSize);
|
||||
glDeleteBuffers(1, &m_outVertices);
|
||||
for (auto& b : m_buffers) {
|
||||
if (b == m_outVertices) b = grown;
|
||||
}
|
||||
m_outVertices = grown;
|
||||
m_outSize = newSize;
|
||||
}
|
||||
|
||||
void Frame(const Fidelity& f, int builders) {
|
||||
if (f.rewriteMapEachFrame) WriteInputs();
|
||||
|
||||
// --- prepareBuffers() -------------------------------------------------
|
||||
const std::vector<IndexedBinding> ssbo =
|
||||
Record(GL_SHADER_STORAGE_BUFFER_BINDING, GL_SHADER_STORAGE_BUFFER_START,
|
||||
GL_SHADER_STORAGE_BUFFER_SIZE, kShaderStorageRestoreRange);
|
||||
const std::vector<IndexedBinding> counters =
|
||||
Record(GL_ATOMIC_COUNTER_BUFFER_BINDING, GL_ATOMIC_COUNTER_BUFFER_START,
|
||||
GL_ATOMIC_COUNTER_BUFFER_SIZE, kAtomicCounterRestoreRange);
|
||||
GLint currentProgram = 0;
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outVertices);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_sharings);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_outVaryings);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, f.leaveMeshesUnbound ? 0 : m_meshes);
|
||||
glUseProgram(m_transform);
|
||||
for (int b = 0; b < builders; ++b) {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inVertices[b]);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_inVaryings[b]);
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexCount"), 4u);
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "vertexOffset"),
|
||||
(GLuint)(4 * b));
|
||||
glProgramUniform1ui(m_transform, glGetUniformLocation(m_transform, "varyingOffset"),
|
||||
(GLuint)(4 * b));
|
||||
glDispatchCompute(1, 1, 1);
|
||||
}
|
||||
glUseProgram(0);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glUseProgram((GLuint)currentProgram);
|
||||
Restore(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
Restore(GL_ATOMIC_COUNTER_BUFFER, counters);
|
||||
|
||||
if (f.regrow) {
|
||||
RegrowOutputBuffer();
|
||||
m_laidOut = false; // isResized() forces the relayout
|
||||
}
|
||||
|
||||
// --- drawBuffers() ----------------------------------------------------
|
||||
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT |
|
||||
GL_COMMAND_BARRIER_BIT);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!m_laidOut || !f.skipRelayout) {
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_outVertices);
|
||||
SetupAttributes();
|
||||
m_laidOut = true;
|
||||
}
|
||||
|
||||
if (f.blendAndDepth) {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
}
|
||||
|
||||
const GLenum indexType = f.shortIndices ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT;
|
||||
const GLuint indexBuffer = f.shortIndices ? m_narrowIndices : m_wideIndices;
|
||||
const GLsizei indexStride = f.shortIndices ? 2 : 4;
|
||||
|
||||
for (int b = 0; b < builders; ++b) {
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); // AutoStorageIndexBuffer.bind
|
||||
glUseProgram(m_blit);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glUniform1i(glGetUniformLocation(m_blit, "Sampler0"), 0);
|
||||
UploadIdentityMatrices();
|
||||
if (f.baseVertex) {
|
||||
// The mod's BASEVERTEX path: every builder reads the SAME first six
|
||||
// indices and offsets the vertices with a base vertex.
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 6, indexType, (const void*)0, 4 * b);
|
||||
} else {
|
||||
glDrawElements(GL_TRIANGLES, 6, indexType, (const void*)(intptr_t)(b * 6 * indexStride));
|
||||
}
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
if (f.blendAndDepth) {
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void UploadIdentityMatrices() {
|
||||
static const float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ModelViewMat"), 1, GL_FALSE, identity);
|
||||
glUniformMatrix4fv(glGetUniformLocation(m_blit, "ProjMat"), 1, GL_FALSE, identity);
|
||||
}
|
||||
|
||||
void ExpectQuads(const Image& image, int builders, const char* when) {
|
||||
EXPECT_TRUE(RegionIsMostly(image, 6, 26, 19, 45, "blue", 0.0,
|
||||
std::string("left accelerated quad, ") + when));
|
||||
if (builders > 1) {
|
||||
EXPECT_TRUE(RegionIsMostly(image, 38, 58, 19, 45, "blue", 0.0,
|
||||
std::string("right accelerated quad, ") + when));
|
||||
}
|
||||
}
|
||||
|
||||
void RunFrames(const Fidelity& f, int builders, int frames, const char* when) {
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Frame(f, builders);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "frame " << i << " raised a GL error (" << when << ")";
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ExpectQuads(image, builders, (std::string(when) + ", frame " + std::to_string(i)).c_str());
|
||||
Gl().EndFrame();
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int m_transform = 0;
|
||||
unsigned int m_blit = 0;
|
||||
ColorFbo m_target{};
|
||||
GLuint m_texture = 0;
|
||||
GLuint m_wideIndices = 0;
|
||||
GLuint m_narrowIndices = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_inVertices[kBuilders] = {};
|
||||
GLuint m_inVaryings[kBuilders] = {};
|
||||
void* m_inVertexMap[kBuilders] = {};
|
||||
void* m_inVaryingMap[kBuilders] = {};
|
||||
GLuint m_sharings = 0;
|
||||
GLuint m_outVertices = 0;
|
||||
GLuint m_outVaryings = 0;
|
||||
GLuint m_meshes = 0;
|
||||
GLsizeiptr m_outSize = 0;
|
||||
bool m_laidOut = false;
|
||||
std::vector<GLuint> m_buffers;
|
||||
std::vector<GLuint> m_vaos;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(GuiBatchScenario, Baseline) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunFrames(Fidelity{}, 1, 1, "baseline");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, ShortIndices) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.shortIndices = true;
|
||||
RunFrames(f, 1, 1, "short indices");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersWideIndices) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
RunFrames(Fidelity{}, 2, 1, "two builders, wide indices");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersBaseVertex) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
RunFrames(f, 2, 1, "two builders, base vertex");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, TwoBuildersShortIndicesBaseVertex) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
RunFrames(f, 2, 1, "two builders, short indices, base vertex");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, BlendAndDepth) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
RunFrames(f, 2, 1, "blend and depth");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, ThreeFramesWithoutRelayout) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.skipRelayout = true;
|
||||
f.rewriteMapEachFrame = true;
|
||||
RunFrames(f, 2, 3, "three frames without relayout");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, MeshesBlockLeftUnbound) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.leaveMeshesUnbound = true;
|
||||
RunFrames(f, 2, 1, "Meshes block left unbound");
|
||||
}
|
||||
|
||||
TEST_F(GuiBatchScenario, FullFidelityWithRegrowth) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
Fidelity f;
|
||||
f.baseVertex = true;
|
||||
f.shortIndices = true;
|
||||
f.blendAndDepth = true;
|
||||
f.skipRelayout = true;
|
||||
f.rewriteMapEachFrame = true;
|
||||
RunFrames(f, 2, 1, "full fidelity, pre-growth");
|
||||
f.regrow = true;
|
||||
RunFrames(f, 2, 1, "full fidelity, growth frame");
|
||||
f.regrow = false;
|
||||
RunFrames(f, 2, 2, "full fidelity, post-growth");
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Atomic counter blocks resolve through the SAME descriptor path as shader storage
|
||||
// blocks (glslang rewrites every atomic_uint into a synthesized storage block), so an
|
||||
// unbound GL_ATOMIC_COUNTER_BUFFER point loses the dispatch for exactly the same reason
|
||||
// an unbound SSBO did. The mod reaches this with its INDIRECT draw method, whose culling
|
||||
// shaders carry a counter the BASEVERTEX default never binds.
|
||||
//
|
||||
// The counter is INCREMENTED, not merely declared: an unreferenced one is optimised out
|
||||
// before it ever reaches a descriptor, so a shader that only declares it proves nothing.
|
||||
constexpr const char* kCounterComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint g_unbound;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
void main() {
|
||||
atomicCounterIncrement(g_unbound);
|
||||
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundCounterBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
GLint counters = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
||||
if (counters < 1) {
|
||||
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
|
||||
}
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &kCounterComputeSource, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
ASSERT_NE(compiled, GL_FALSE);
|
||||
m_program = glCreateProgram();
|
||||
glAttachShader(m_program, shader);
|
||||
glLinkProgram(m_program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_NE(linked, GL_FALSE);
|
||||
glGenBuffers(1, &m_buffer);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_buffer) glDeleteBuffers(1, &m_buffer);
|
||||
if (m_program) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
GLuint m_buffer = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(UnboundCounterBlockScenario, ADeclaredButUnboundCounterDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kElements = 4;
|
||||
const std::vector<unsigned int> zeros((std::size_t)kElements, 0u);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
|
||||
// Nothing is bound at GL_ATOMIC_COUNTER_BUFFER point 0 on purpose.
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(kElements, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
|
||||
|
||||
std::vector<unsigned int> values((std::size_t)kElements, 0xDEADBEEFu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)(values.size() * sizeof(unsigned int)),
|
||||
values.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
EXPECT_EQ(values[(std::size_t)i], (unsigned int)(i + 1))
|
||||
<< "element " << i << " came back as " << values[(std::size_t)i]
|
||||
<< "; zero everywhere means the whole dispatch was dropped over the unbound counter block";
|
||||
}
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,391 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER.
|
||||
//
|
||||
// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border
|
||||
// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and
|
||||
// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754
|
||||
// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on
|
||||
// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK.
|
||||
//
|
||||
// Two independent halves, and this scenario covers both because it goes through the frontend:
|
||||
//
|
||||
// * the STATE had no record of which entry point wrote the border colour. All three
|
||||
// representations are kept numerically in step, so the value alone cannot say whether the
|
||||
// application called glTexParameterfv or glTexParameterIiv.
|
||||
// * each backend then had exactly one border-colour call site: glTexParameterfv /
|
||||
// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on
|
||||
// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all.
|
||||
//
|
||||
// The border value is deliberately outside every predefined VkBorderColor and outside anything a
|
||||
// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black,
|
||||
// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color.
|
||||
// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit.
|
||||
//
|
||||
// Both an integer image view and an integer border colour are involved, which is the other half of
|
||||
// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour
|
||||
// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK.
|
||||
// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kOutputWidth = 8;
|
||||
constexpr int kOutputHeight = 8;
|
||||
|
||||
// The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border
|
||||
// is deliberately not derivable from the texel.
|
||||
constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44};
|
||||
constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3};
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// One channel per draw, so a failure names the component that is wrong. The coordinate is a
|
||||
// uniform rather than a literal so the same program serves the border sample and the inside
|
||||
// sample and nothing can be constant-folded differently between them.
|
||||
std::string FragmentSource(int channel) {
|
||||
static const char* kChannels[4] = {"x", "y", "z", "w"};
|
||||
return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n"
|
||||
"out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") +
|
||||
kChannels[channel] + ";\n}\n";
|
||||
}
|
||||
|
||||
class IntegerBorderColorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
// 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory.
|
||||
const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}};
|
||||
glGenTextures(1, &m_sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
|
||||
|
||||
// 8x8 R32I render target: an integer readback, so nothing is normalized on the way
|
||||
// out and a wrong value is reported as the number it actually was.
|
||||
glGenTextures(1, &m_outputTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_sampler != 0) {
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &m_sampler);
|
||||
m_sampler = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
|
||||
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
|
||||
if (m_narrowTexture != 0) glDeleteTextures(1, &m_narrowTexture);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// Samples `coord` through the integer sampler and returns every texel the draw wrote.
|
||||
std::vector<std::int32_t> RenderChannel(int channel, float coordX, float coordY) {
|
||||
const std::string fragment = FragmentSource(channel);
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "channel " << channel << ": program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
// A clear value nothing under test can produce, so an undrawn target is not mistaken
|
||||
// for a correct one.
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, int channel, std::int32_t expected,
|
||||
const std::vector<std::int32_t>& texels) {
|
||||
if (texels.empty()) return;
|
||||
std::size_t offenders = 0;
|
||||
std::int32_t firstBad = 0;
|
||||
for (const std::int32_t texel : texels) {
|
||||
if (texel == expected) continue;
|
||||
if (offenders == 0) firstBad = texel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad
|
||||
<< " instead of " << expected << " (" << offenders << " of " << texels.size()
|
||||
<< " texels wrong)";
|
||||
}
|
||||
|
||||
// Every component of the border, in one place, so both the texture-object and the
|
||||
// sampler-object case assert exactly the same thing.
|
||||
void ExpectBorderIsDelivered(const char* what) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
// (-0.5, -0.5) is a full texture width outside the image on both axes, so
|
||||
// CLAMP_TO_BORDER can only answer with the border colour.
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, -0.5f, -0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind";
|
||||
ExpectAllTexels(what, channel, kBorderColor[channel], texels);
|
||||
}
|
||||
}
|
||||
|
||||
// A narrow-format source built on demand, for the clamp cases. Returns the texture, which
|
||||
// the caller owns until TearDown deletes it through m_narrowTexture.
|
||||
void MakeNarrowSource(GLenum internalFormat, GLenum clientFormat, const void* texels,
|
||||
const GLint* border, bool borderIsUnsigned) {
|
||||
glGenTextures(1, &m_narrowTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, clientFormat,
|
||||
internalFormat == GL_R8UI ? GL_UNSIGNED_BYTE : GL_BYTE, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
if (borderIsUnsigned) {
|
||||
const GLuint asUnsigned[4] = {static_cast<GLuint>(border[0]), static_cast<GLuint>(border[1]),
|
||||
static_cast<GLuint>(border[2]), static_cast<GLuint>(border[3])};
|
||||
glTexParameterIuiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, asUnsigned);
|
||||
} else {
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "narrow source setup left a GL error behind";
|
||||
}
|
||||
|
||||
// The narrow sources are single-channel, so only component 0 carries anything, and the
|
||||
// sampler declaration has to match the format's signedness.
|
||||
std::vector<std::int32_t> RenderNarrowBorder(bool isUnsignedSampler) {
|
||||
const std::string fragment =
|
||||
std::string("#version 330 core\n\nuniform ") + (isUnsignedSampler ? "usampler2D" : "isampler2D") +
|
||||
" smp;\nuniform vec2 uCoord;\n\nout int out_color;\n\nvoid main()\n{\n"
|
||||
" out_color = int(texture(smp, uCoord).x);\n}\n";
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "narrow-border program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), -0.5f, -0.5f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
GLuint m_sourceTexture = 0;
|
||||
GLuint m_outputTexture = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_sampler = 0;
|
||||
GLuint m_narrowTexture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE
|
||||
// sample has to fetch the texture's own texel. If this fails the sampler, the shader or the
|
||||
// integer readback is broken and nothing about the border colour has been measured.
|
||||
TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, 0.5f, 0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind";
|
||||
ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels);
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend
|
||||
// already accepted and then flattened into the same FloatVec4 every other spelling wrote.
|
||||
TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glTexParameterIiv");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, sampler-object spelling - which is the one the conformance cases actually use,
|
||||
// and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the
|
||||
// sampler cache key on DirectVulkan, where a border colour that is not part of the key would
|
||||
// alias two samplers that differ only in it).
|
||||
TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glGenSamplers(1, &m_sampler);
|
||||
ASSERT_NE(m_sampler, 0u);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected";
|
||||
|
||||
// The texture object carries a DIFFERENT border colour, so a pass here cannot come from the
|
||||
// texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state
|
||||
// wins over the texture's for every sampling parameter.
|
||||
const std::int32_t decoyBorder[4] = {0, 0, 0, 0};
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder);
|
||||
glBindSampler(0, m_sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glSamplerParameterIiv");
|
||||
glBindSampler(0, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "For floating-point and integer formats, border values are clamped to the
|
||||
// representable range of the format." A border of 300 on a GL_R8I texture is 127, not 300 - and
|
||||
// VK_BORDER_COLOR_INT_CUSTOM_EXT delivers whatever it is handed, with format VK_FORMAT_UNDEFINED
|
||||
// there is nothing for the driver to clamp against, so the clamp has to happen before the value
|
||||
// leaves MobileGL. DirectGLES gets it right for free (the ES driver knows the texture format),
|
||||
// which is what makes this a cross-backend divergence and not only a spec one.
|
||||
TEST_F(IntegerBorderColorScenario, ASignedIntegerBorderIsClampedToTheFormatsRepresentableRange) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::int8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {300, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8I, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/false);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8I border 300", 0, 127, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The reciprocal half, and the one that decides how the two integer forms relate: -1 written
|
||||
// through glTexParameterIiv against an UNSIGNED format. GL 4.6 core 8.10 stores an "I"-form
|
||||
// border unmodified with an integer internal data type and defines no sign conversion between
|
||||
// the two integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness: 0xFFFFFFFF, clamped to the format's maximum of 255.
|
||||
//
|
||||
// That is the DRIVER's answer, established by running this case rather than by reading the spec:
|
||||
// clamping to 0 is an equally defensible reading of the same paragraph, and DirectVulkan can be
|
||||
// made to produce either - but DirectGLES forwards the value to the ES driver verbatim and cannot
|
||||
// deviate, so choosing 0 would mean the same program sampling 0 on Magma and 255 on Espryt. The
|
||||
// whole point of carrying the border colour's form is to stop that class of divergence, so the
|
||||
// backends agree on the driver's answer.
|
||||
//
|
||||
// The clamp itself is still doing the work: without it the value reaches the driver as
|
||||
// 0xFFFFFFFF against a format whose maximum is 255, with format VK_FORMAT_UNDEFINED and so
|
||||
// nothing for the driver to clamp against.
|
||||
TEST_F(IntegerBorderColorScenario, ANegativeBorderOnAnUnsignedFormatClampsToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {-1, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border -1", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same clamp from the unambiguous side: a value written through the UNSIGNED form that is
|
||||
// simply too large for the format. No sign reinterpretation is involved, so both backends and
|
||||
// the spec agree that 5000 on a GL_R8UI texture is 255.
|
||||
TEST_F(IntegerBorderColorScenario, AnOversizedUnsignedBorderIsClampedToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {5000, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/true);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border 5000", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -137,7 +137,7 @@ namespace MGITest {
|
||||
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
||||
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
|
||||
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
|
||||
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
|
||||
// the pack itself replays through the MagmaFixIterationRPSubgroupScratch patch, which
|
||||
// this probe deliberately does not model. The width only gates the domain;
|
||||
// lane placement and group counts still come from observed values alone.
|
||||
bool SubgroupWidthInSourceDomain() const {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - MESH-ARENA-SIZED BUFFERS, END TO END.
|
||||
//
|
||||
// A buffer store of at least 16MiB is adopted into the backend's persistently and
|
||||
// coherently mapped GPU storage the moment it is defined (BufferObject::
|
||||
// TryAdoptLargeStorage): the CPU shadow is dropped and every later write lands
|
||||
// directly in GPU-visible memory with no per-write driver call. Minecraft 26.3
|
||||
// streams chunk meshes into 128MB vertex arenas with plain glNamedBufferSubData -
|
||||
// on Mali, every driver-mediated route for that write into a busy mutable store
|
||||
// either parks the calling thread or ghost-copies the whole arena on a driver
|
||||
// worker (~167ms per touched arena: the recurring in-world hiccup this adoption
|
||||
// removed). Every existing buffer scenario uses stores far below the threshold,
|
||||
// so without this file the adopted path would have zero coverage.
|
||||
//
|
||||
// What is pinned, deliberately through the same API mix Minecraft uses:
|
||||
// * a glBufferSubData written AFTER the arena was drawn (in flight) reaches the
|
||||
// next draw - the write-visibility contract adoption must not weaken;
|
||||
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
|
||||
// * a compute-shader write through an SSBO binding of the same arena is read
|
||||
// back - the GPU-written path for adopted stores (glFinish + direct read).
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Comfortably past the 16MiB adoption threshold, and the vertex payload sits
|
||||
// deep inside the store so an implementation that quietly clamped or aliased
|
||||
// the adopted range would miss it.
|
||||
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(24) * 1024 * 1024;
|
||||
constexpr GLintptr kVertexOffset = GLintptr(20) * 1024 * 1024;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec3 a_color;
|
||||
out vec3 v_color;
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||
in vec3 v_color;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(v_color, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kMarkerComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Arena { uint word; };
|
||||
void main() { word = 0xC0FFEEu; }
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
float r, g, b;
|
||||
};
|
||||
|
||||
// A full-viewport quad, colored uniformly so one center readback speaks for
|
||||
// the whole draw.
|
||||
std::vector<Vertex> QuadVertices(float r, float g, float b) {
|
||||
return {
|
||||
{-1.f, -1.f, r, g, b}, {1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b},
|
||||
{-1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b}, {-1.f, 1.f, r, g, b},
|
||||
};
|
||||
}
|
||||
|
||||
class LargeArenaAdoptionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = LinkProgram(kVertexSource, kFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_arena);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
// The NULL-data definition is the adoption point (and Minecraft's
|
||||
// arena-creation idiom).
|
||||
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<void*>(kVertexOffset));
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_compute != 0) glDeleteProgram(m_compute);
|
||||
m_vao = 0;
|
||||
m_arena = 0;
|
||||
m_program = 0;
|
||||
m_compute = 0;
|
||||
}
|
||||
|
||||
unsigned int CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
unsigned int LinkProgram(const char* vs, const char* fs) {
|
||||
const GLuint v = CompileStage(GL_VERTEX_SHADER, vs);
|
||||
if (v == 0) return 0;
|
||||
const GLuint f = CompileStage(GL_FRAGMENT_SHADER, fs);
|
||||
if (f == 0) {
|
||||
glDeleteShader(v);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, v);
|
||||
glAttachShader(program, f);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(v);
|
||||
glDeleteShader(f);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
void UploadQuad(float r, float g, float b) {
|
||||
const auto vertices = QuadVertices(r, g, b);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
}
|
||||
|
||||
void DrawQuad() {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
std::array<unsigned char, 4> CenterPixel() {
|
||||
std::array<unsigned char, 4> px = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
px.data());
|
||||
return px;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_compute = 0;
|
||||
unsigned int m_vao = 0;
|
||||
unsigned int m_arena = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The Minecraft shape: the arena is drawn, the frame retires, and a
|
||||
// glBufferSubData rewrites the SAME vertex bytes while the previous frame's
|
||||
// draw may still be in flight. The next draw must show the NEW bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, SubDataAfterAnInFlightDrawReachesTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
DrawQuad();
|
||||
auto px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[0], 200) << "the first draw from the adopted arena never landed";
|
||||
EXPECT_LT(px[1], 50);
|
||||
|
||||
Gl().EndFrame();
|
||||
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
DrawQuad();
|
||||
px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[1], 200) << "the cross-frame rewrite of the adopted arena did not reach the draw; "
|
||||
"the old color means the write went to bytes the draw no longer reads";
|
||||
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||
}
|
||||
|
||||
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
||||
// back exactly those bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const auto vertices = QuadVertices(0.25f, 0.5f, 0.75f);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
std::vector<Vertex> read(vertices.size());
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(read.size() * sizeof(Vertex)), read.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(0, std::memcmp(read.data(), vertices.data(), read.size() * sizeof(Vertex)))
|
||||
<< "GetBufferSubData of the adopted arena returned different bytes than the SubData wrote";
|
||||
}
|
||||
|
||||
// A GPU write through an SSBO binding of the adopted arena must be visible to
|
||||
// a CPU readback - the path that waits out the GPU and reads the coherent
|
||||
// mapping directly.
|
||||
TEST_F(LargeArenaAdoptionScenario, GpuWriteIntoTheArenaIsReadBack) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLint maxComputeStorageBlocks = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
|
||||
if (maxComputeStorageBlocks < 1) {
|
||||
GTEST_SKIP() << "no compute shader storage blocks on this driver";
|
||||
}
|
||||
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
|
||||
ASSERT_NE(compute, 0u) << m_buildLog;
|
||||
m_compute = glCreateProgram();
|
||||
glAttachShader(m_compute, compute);
|
||||
glLinkProgram(m_compute);
|
||||
glDeleteShader(compute);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_compute, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
|
||||
const unsigned int seed = 0u;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(seed), &seed);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
|
||||
glUseProgram(m_compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
unsigned int marker = 0;
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(marker, 0xC0FFEEu)
|
||||
<< "the compute write into the adopted arena did not reach the CPU readback";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
File diff suppressed because it is too large
Load Diff
@@ -518,5 +518,62 @@ void main() {
|
||||
ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws");
|
||||
}
|
||||
|
||||
// The base-vertex family's argument checks (GL 4.6 core 10.3.9). These are what
|
||||
// KHR-GL4x.draw_elements_base_vertex_tests.invalid_* assert, and the reason the group sat
|
||||
// NotSupported for so long hid the fact that the entry points forwarded any argument
|
||||
// straight to the backend: a negative count reached the emulation as a huge unsigned
|
||||
// size. Each case drains the error queue first so the assertion names the call it made.
|
||||
TEST_F(MultiDrawScenario, BaseVertexDrawsRejectMalformedArguments) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPad = 0;
|
||||
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||
// A bound program and VAO are prerequisites, not decoration: the entry points check
|
||||
// "is there something to execute" (GL_INVALID_OPERATION) before they look at any
|
||||
// argument, so without these every case below would pass for the wrong reason.
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
|
||||
const auto expectError = [&](const char* what, GLenum expected) {
|
||||
EXPECT_EQ(FirstGLError(), expected) << what;
|
||||
// FirstGLError stops at the first one; make sure nothing else is queued so the
|
||||
// next case starts clean.
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
};
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, -1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a negative count", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 3, GL_NONE, nullptr, 0);
|
||||
expectError("glDrawElementsBaseVertex with a non-index type", GL_INVALID_ENUM);
|
||||
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, 3, 0, 3, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with end < start", GL_INVALID_VALUE);
|
||||
|
||||
// start = -1 arrives as 0xFFFFFFFF, so this is the same end < start rule seen from
|
||||
// the other side - and it is the shape the CTS's invalid_count case actually uses.
|
||||
glDrawRangeElementsBaseVertex(GL_TRIANGLES, static_cast<GLuint>(-1), 2, 1, GL_UNSIGNED_INT, nullptr, 0);
|
||||
expectError("glDrawRangeElementsBaseVertex with a wrapped start", GL_INVALID_VALUE);
|
||||
|
||||
glDrawElementsInstancedBaseVertex(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr, -1, 0);
|
||||
expectError("glDrawElementsInstancedBaseVertex with a negative instancecount", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei negativeCount = -1;
|
||||
const void* offsets[1] = {reinterpret_cast<const void*>(0)};
|
||||
const GLint baseVertices[1] = {0};
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &negativeCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative element of count", GL_INVALID_VALUE);
|
||||
|
||||
const GLsizei validCount = 6;
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, -1, baseVertices);
|
||||
expectError("glMultiDrawElementsBaseVertex with a negative drawcount", GL_INVALID_VALUE);
|
||||
|
||||
// The well-formed call still has to go through, or the checks above would be
|
||||
// indistinguishable from a blanket rejection.
|
||||
glMultiDrawElementsBaseVertex(GL_TRIANGLES, &validCount, GL_UNSIGNED_INT, offsets, 1, baseVertices);
|
||||
expectError("a well-formed glMultiDrawElementsBaseVertex", GL_NO_ERROR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.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 MOBILEGL_PIPE_VERIFY COMPARATOR IS ARMED, AND SAYS SO, AND CAN GO RED.
|
||||
//
|
||||
// The third CI mode (ARCHITECTURE.md 13.2-(2)) runs the whole integration suite with two state
|
||||
// models in one address space: the PipeInputs block the frontend fills at every verb boundary,
|
||||
// and a SnapshotFromGLContext() taken from the live GLContext. A green run of that mode is only
|
||||
// worth something if the comparator was actually RUNNING - and "MOBILEGL_PIPE_VERIFY=1 against a
|
||||
// library that was not built with -DMOBILEGL_PIPE_VERIFY=ON" is a no-op that looks exactly like a
|
||||
// clean pass. That is the failure mode this scenario exists to make impossible:
|
||||
//
|
||||
// Armed - the environment says the comparator is on for this process, so the
|
||||
// library must SAY it armed. It asserts a library observable against
|
||||
// the environment, the same shape UnlocatedIoBlockScenario's arming
|
||||
// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration
|
||||
// use. A lane whose library never armed FAILS here; it never passes.
|
||||
// CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is
|
||||
// perturbed before the entry compare, so a comparator that works must
|
||||
// report Fatal{PipeVerifyDiffer, "<Field>@<Verb>"}. A comparator that
|
||||
// compares nothing stays quiet and this case goes red.
|
||||
//
|
||||
// The observable is the library's own log, because MG_Config is not reachable from this module
|
||||
// (on Android it links the SHIPPING libMobileGL.so, built -fvisibility=hidden) and the arming
|
||||
// signal is a latched MGLOG_I. The ctest entry sets MOBILEGL_LOG_FILE_PATH; this only reads it.
|
||||
//
|
||||
// Note on scope, and why Armed runs in a lane of its own. The log file is opened with
|
||||
// fopen(path, "w") at the first log write of a process (MG_Util/Debug/Log.cpp, InitFile), so each
|
||||
// process TRUNCATES it. That is fine for one process and false for many: in the ambient Verify.
|
||||
// lane, 400-odd sibling entries share the one MOBILEGL_LOG_FILE_PATH, and CI runs that lane with
|
||||
// `ctest -j 4`, so a neighbour's bring-up can truncate the file between this case's draw and its
|
||||
// read. Every existing scenario in this suite that reads the library log (UnlocatedIoBlockScenario,
|
||||
// the primgen reroute, the point-size demotion) is registered in a FILTERED lane with a log path of
|
||||
// its own for exactly that reason, and this case now follows them: it runs in the VerifyArming.
|
||||
// entries, which set MGITEST_PIPE_ARMING_LANE=1 and their own log, and skips everywhere else.
|
||||
//
|
||||
// What that proves, stated honestly: the arming line is a property of (this library, this
|
||||
// environment), not of an individual test body, and the VerifyArming. entry runs the same library
|
||||
// with the same MOBILEGL_PIPE_VERIFY=1 as its ~400 ambient siblings. One process per backend is
|
||||
// therefore the whole of the evidence available for "the lane armed" - the per-process claim the
|
||||
// shared log CANNOT support, because it only ever holds the last writer.
|
||||
//
|
||||
// Within the process: the arming line is latched at the FIRST fill, which may be the harness
|
||||
// bring-up rather than this test's draw, so the arming search is whole-file on purpose; the
|
||||
// divergence search is restricted to the bytes this case appended, which is where a differ belongs.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The three strings the comparator contracts to print (the brief's D8 reporting shape).
|
||||
// They are spelled here once so a rename of either half is one compile-visible edit.
|
||||
constexpr const char* kArmedLine = "MGPipe: verify armed";
|
||||
constexpr const char* kDifferPrefix = "Fatal{PipeVerifyDiffer";
|
||||
constexpr const char* kUnmigratedPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
// Set by the VerifyArming. ctest entries and by nothing else. It is a HARNESS variable, not
|
||||
// a library knob (hence the MGITEST_ prefix): the library never reads it. It exists because
|
||||
// this case reads a log file, and a log file is a per-LANE resource - see the note at the
|
||||
// top of the file.
|
||||
constexpr const char* kArmingLaneMarker = "MGITEST_PIPE_ARMING_LANE";
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
// Reads the environment the way MG_ConfigLoader does (ScenarioFixture.h documents the
|
||||
// rule); a string knob is "set" when it is present and non-empty, which is exactly what
|
||||
// MG_ConfigLoader's QueryEnvVariable turns into a non-empty Features member.
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
class PipeVerifyArmingScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The library log this process is writing, or an empty path when none was configured.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static std::string LibraryLog() { return LibraryLogSince(0); }
|
||||
|
||||
// One frame that crosses several verb boundaries: a clear (kClear), a draw (kDraw) and
|
||||
// a readback (kReadback). Three of the nine fill classes, so an entry compare that only
|
||||
// ran for one of them still has something to say.
|
||||
void DrawOneFrame() {
|
||||
HeadlessGL& gl = Gl();
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
m_centre = pixel;
|
||||
}
|
||||
|
||||
Rgba8 m_centre{};
|
||||
};
|
||||
|
||||
// THE CASE THAT FAILS A LANE WHOSE LIBRARY NEVER ARMED.
|
||||
//
|
||||
// Every other entry in the integration-verify lane renders the same frames it renders in the
|
||||
// ambient lane and would be just as green against a library with no comparator compiled in -
|
||||
// which is precisely how a verify lane goes green having verified nothing. This case is the
|
||||
// one that cannot: the environment pins MOBILEGL_PIPE_VERIFY=1, therefore the library must
|
||||
// have said "MGPipe: verify armed" in its own log, and if it did not, the mode is not running.
|
||||
TEST_F(PipeVerifyArmingScenario, Armed) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet(kArmingLaneMarker)) {
|
||||
GTEST_SKIP() << "this case reads the library's log file, so it runs in the VerifyArming. "
|
||||
"lane, which owns a log path no other entry writes to. In the ambient "
|
||||
"Verify. lane 400-odd entries share one path and each truncates it "
|
||||
"(Log.cpp opens it \"w\"), so a whole-file read here would race a "
|
||||
"neighbour under ctest -j 4. Set by the ctest entry, never by hand.";
|
||||
}
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs MOBILEGL_PIPE_VERIFY=1 for the whole process, which is "
|
||||
"what the Verify. ctest entries set; with the variable unset the "
|
||||
"comparator is dormant even in a build that compiled it in";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY is pinned on but MOBILEGL_LOG_FILE_PATH is not "
|
||||
"set, so the library has nowhere to record that it armed; the Verify. "
|
||||
"ctest entries set both";
|
||||
}
|
||||
if (StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is armed in this process, so a divergence "
|
||||
"is the EXPECTED outcome and asserting on its absence here would be "
|
||||
"backwards; the VerifyCorrupted. lane owns that half";
|
||||
}
|
||||
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Whole file, not just the appended bytes: the arming line is latched at the FIRST fill
|
||||
// of the process, which may already have happened during the harness bring-up. The file
|
||||
// is truncated at this process's first log write, so it still carries nothing else.
|
||||
const std::string whole = LibraryLog();
|
||||
EXPECT_NE(whole.find(kArmedLine), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and "
|
||||
"read back, and the library never reported arming the comparator. Either this "
|
||||
"library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane "
|
||||
"is verifying nothing), or the arming MGLOG_I is gone. Log:\n"
|
||||
<< whole;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos)
|
||||
<< "the comparator reported a push/pull divergence on an ordinary frame:\n"
|
||||
<< appended;
|
||||
EXPECT_EQ(appended.find(kUnmigratedPrefix), std::string::npos)
|
||||
<< "a backend read a field the verb's fill table does not list (add the row to "
|
||||
"MG_Pipe/FillPoints.def, never mark the field sticky):\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
// NEGATIVE CONTROL A (gate G4): a deliberately corrupted snapshot field must turn a green
|
||||
// verify run red, naming that field and the verb it diverged on.
|
||||
//
|
||||
// It runs in its own lane (VerifyCorrupted.) because the knob is process-wide, and with
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and this case can
|
||||
// read the report back out of the log. The CI step that runs the SAME knob against the
|
||||
// ambient lane - where FATAL keeps its default - asserts the other half: there, the
|
||||
// divergence must abort and ctest must go red.
|
||||
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReported) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "this case is the comparator's negative control and needs "
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=<FieldName> for the whole process, which "
|
||||
"is what the VerifyCorrupted. ctest entries set";
|
||||
}
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is set but MOBILEGL_PIPE_VERIFY is not, so "
|
||||
"the comparator is dormant and there is nothing to corrupt";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to report "
|
||||
"the divergence; the VerifyCorrupted. ctest entries set both";
|
||||
}
|
||||
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_VERIFY_CORRUPT");
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
const std::string expected = std::string(kDifferPrefix) + ", \"" + knob + "@";
|
||||
EXPECT_NE(appended.find(expected), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY_CORRUPT=" << knob
|
||||
<< " perturbs that field in the snapshot arm before every entry compare, so a working "
|
||||
"comparator must have reported " << expected << "...\". It reported nothing, which "
|
||||
"means the comparator is not comparing - and every green entry in this lane is "
|
||||
"green for no reason. Log appended by this case:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,522 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PointSizeDemotionScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - THE gl_PointSize DEMOTION IS CLIENT-INVISIBLE, AND IT ACTUALLY ARMS.
|
||||
//
|
||||
// On a device that hosts the built-in in tessellation/geometry stages (llvmpipe and
|
||||
// lavapipe both do), gl_PointSize travels as itself; on one that does not (the Mali
|
||||
// devices this exists for), phase B demotes it to an ordinary varying
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram) and the capture
|
||||
// machinery follows it there. This scenario runs in BOTH configurations and must hand
|
||||
// back identical bytes: the ambient registrations take the native path, and the
|
||||
// PointSizeDemotion. registrations pin MOBILEGL_POINT_SIZE_DEMOTION=1 so the demotion
|
||||
// runs on the same healthy drivers - CopyImagePacked16Scenario's dual-configuration
|
||||
// contract, applied to a value chain instead of a storage format.
|
||||
//
|
||||
// The VALUE is the whole contract: every case writes gl_PointSize in one stage, reads it
|
||||
// back out of gl_in[] in the next, and captures it by name under rasterizer discard, so
|
||||
// one wrong link anywhere in VS -> TCS -> TES -> GS -> capture lands in the readback.
|
||||
// The RASTERIZED size is deliberately not asserted anywhere: with the built-in unhosted
|
||||
// it falls back to 1.0 by spec on both targets, which is exactly the honest residue the
|
||||
// demotion documents (point_rendering-style bodies keep failing truthfully).
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on
|
||||
// the absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 460 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The full chain, with per-vertex VARIATION seeded in the vertex stage so a control
|
||||
// invocation that read or wrote the wrong slot changes the sum: 2,3,4 arrive, 3,4,5
|
||||
// leave, the evaluation stage sums its patch to 12, the geometry stage doubles what
|
||||
// it read to 24.
|
||||
const char* const kChainVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 2.0 + float(gl_VertexID);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessControlSource = R"(#version 460 core
|
||||
layout(vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessEvalSource = R"(#version 460 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// The geometry-only chain: no tessellation required of the stack at all.
|
||||
const char* const kPointVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 7.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// A capture stage that only READS the incoming point size and never writes its own.
|
||||
// Legal GL, and the shape that separates "the demotion arms" from "the demotion knows
|
||||
// a capture is coming": with the built-in gone, only the capture request can put a
|
||||
// carrier back for a by-name capture to bind to.
|
||||
const char* const kReadOnlyGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float g_echo;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
g_echo = gl_in[0].gl_PointSize;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kEchoFragmentSource = R"(#version 460 core
|
||||
in float g_echo;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(g_echo, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PointSizeDemotionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()),
|
||||
varyings.data(), GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over `vertexCount` vertices of `drawMode`, recorded as
|
||||
// GL_POINTS. The buffer is poison-filled first so bytes the capture never wrote
|
||||
// name themselves.
|
||||
std::vector<float> RunCaptureSpan(GLuint program, GLenum drawMode, GLsizei vertexCount,
|
||||
std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(drawMode, 0, vertexCount);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data,
|
||||
std::size_t index, float expected,
|
||||
float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture "
|
||||
<< "never reached these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// The library log, for the arming case. Same machinery and same reasoning as
|
||||
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, the file
|
||||
// is appended to by every process in the lane, and only bytes appended after the
|
||||
// snapshot may satisfy an assertion.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
|
||||
private:
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
};
|
||||
|
||||
// The five-stage chain. 24.0 can only arrive if the vertex mirror, both control-stage
|
||||
// redirects (read AND write), the evaluation stage's three gl_in reads and the
|
||||
// geometry stage's read all carried the right value - one wrong link and the sum
|
||||
// moves. point_mode with every level at 1 emits three points; the first record proves
|
||||
// the mechanism, exactly as TessellationXfbCaptureScenario reasons.
|
||||
TEST_F(PointSizeDemotionScenario, TheValueSurvivesTheFiveStageChainIntoTheCapture) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_GEOMETRY_SHADER, kChainGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 24.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same chain without a geometry stage: the capture then binds to the evaluation
|
||||
// stage's value (the sum, 12.0) - which is also the boundary where a demoted program
|
||||
// switches its capture carrier from the Io chain to the capture name.
|
||||
TEST_F(PointSizeDemotionScenario, TheEvaluationStageOwnsTheCaptureWithoutAGeometryStage) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 12.0f));
|
||||
|
||||
// The GL query surface keeps the truthful spelling whatever the backends renamed
|
||||
// underneath: reflection is a phase-A product and the demotion happens after it.
|
||||
char varyingName[64] = {};
|
||||
GLsizei nameLength = 0;
|
||||
GLsizei varyingSize = 0;
|
||||
GLenum varyingType = 0;
|
||||
glGetTransformFeedbackVarying(program, 0, sizeof(varyingName), &nameLength, &varyingSize,
|
||||
&varyingType, varyingName);
|
||||
EXPECT_STREQ(varyingName, "gl_PointSize");
|
||||
EXPECT_EQ(varyingType, static_cast<GLenum>(GL_FLOAT));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The geometry-only chain: gl_in[0].gl_PointSize read straight off the vertex stage,
|
||||
// no tessellation involved - the VS -> GS boundary of the demotion on its own.
|
||||
TEST_F(PointSizeDemotionScenario, AGeometryOnlyChainCarriesTheVertexValue) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE CAPTURE-REQUEST PATH, END TO END - the half no unit test can reach, because the
|
||||
// request travels from glTransformFeedbackVaryings through phase A's resolved capture
|
||||
// set and the phase-B handoff before it reaches the demotion.
|
||||
//
|
||||
// The geometry stage READS gl_in[0].gl_PointSize and never writes gl_PointSize, which
|
||||
// is enough to arm the demotion (glslang declares GeometryPointSize on a read) but not
|
||||
// enough to create an output carrier on its own. Only the capture request can, and if
|
||||
// that request never arrives the program does not merely lose the point-size column:
|
||||
// DirectGLES respells the driver-side capture to a name no stage declares and the
|
||||
// WHOLE capture set fails to link, while DirectVulkan mirrors a built-in the demotion
|
||||
// just removed and can unwind far enough to drop the Xfb execution mode. Either way
|
||||
// g_echo - an ordinary varying with nothing to do with point size - comes back poison,
|
||||
// which is what this asserts. gl_PointSize itself is captured but never asserted: no
|
||||
// stage writes it, so GL leaves its value undefined.
|
||||
TEST_F(PointSizeDemotionScenario, ACaptureSurvivesAStageThatOnlyReadsThePointSize) {
|
||||
if (!Ready()) return;
|
||||
// The NATIVE Espryt path cannot do this at all, and never could: with the built-in
|
||||
// hosted, the geometry stage's ESSL simply does not declare gl_PointSize unless it
|
||||
// writes it, so the driver rejects the capture request with "varying undeclared"
|
||||
// and the program becomes unusable. That is a pre-existing ES limitation the
|
||||
// demotion happens to REPAIR - the carrier is a real, seeded, declared varying -
|
||||
// so this case has something to assert only where the demotion is armed. Magma
|
||||
// consumes SPIR-V and answers on both paths, which keeps the negative control.
|
||||
if (Gl().BackendName() == "DirectGLES" &&
|
||||
AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "Espryt cannot capture a gl_PointSize its capture stage never "
|
||||
"writes without the demotion; the PointSizeDemotion. ctest entry "
|
||||
"runs this same case with MOBILEGL_POINT_SIZE_DEMOTION=1";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kReadOnlyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kEchoFragmentSource}},
|
||||
{"g_echo", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "the capture set failed to link. On a demoting configuration this is the "
|
||||
"capture request never reaching the demotion, so the point-size capture was "
|
||||
"respelled to a carrier no stage declares. Build log: "
|
||||
<< m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 2);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f))
|
||||
<< "the unrelated varying captured alongside gl_PointSize did not survive; the "
|
||||
"point-size capture took the whole set with it";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE DEMOTION SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above captures the right bytes on llvmpipe and lavapipe whether the
|
||||
// demotion ran or not - these machines host the built-in - so those cases pin that
|
||||
// the demotion does no HARM and can say nothing about whether it happened. The
|
||||
// arming is where the cheap mistake lives: MOBILEGL_POINT_SIZE_DEMOTION maps onto
|
||||
// the two Supports*PointSize capability bits INVERTED (forcing the demotion on
|
||||
// means declaring the built-in UNHOSTED), and a swap of those arms - or a dropped
|
||||
// env bit anywhere between ConfigLoader, the backend init, CompileEnv and the L1
|
||||
// key - would disable the device repair with every rendering case still green.
|
||||
//
|
||||
// Same machinery as UnlocatedIoBlockScenario's arming case: the environment says
|
||||
// the demotion is pinned on, therefore the library must SAY it demoted something.
|
||||
// The observable is the latched MGLOG_I each backend emits when it first builds a
|
||||
// demoted program; both spell "demoted to an ordinary varying", so this one case
|
||||
// covers both pinned lanes without a backend gate.
|
||||
TEST_F(PointSizeDemotionScenario, TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the demotion pinned ON for the whole process, which "
|
||||
"is what the PointSizeDemotion. ctest entries do with "
|
||||
"MOBILEGL_POINT_SIZE_DEMOTION=1; with the variable unset the detected "
|
||||
"capabilities decide, and on this machine the built-in is hosted - so "
|
||||
"there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_POINT_SIZE_DEMOTION is pinned on but MOBILEGL_LOG_FILE_PATH "
|
||||
"is not set, so the library has nowhere to record that it demoted "
|
||||
"anything; the PointSizeDemotion. ctest entries set both";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
// Drawn as well as built, so a stack that defers its backend program to first
|
||||
// use still reaches the build the latched line fires in - and the capture must
|
||||
// STILL be right through the carrier.
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f))
|
||||
<< "the pinned-on lane did not even capture correctly";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("demoted to an ordinary varying"), std::string::npos)
|
||||
<< "MOBILEGL_POINT_SIZE_DEMOTION is pinned ON, a geometry program reading and "
|
||||
"writing gl_PointSize was built and captured, and no backend ever reported "
|
||||
"demoting it. The demotion is not armed - check the override mapping in the "
|
||||
"backend inits (it is inverted on purpose), the CompileEnv accessors, and "
|
||||
"ProgramSpirvTask's verdict plumbing. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,413 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PoisonOmissionScenario.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 - NEGATIVE CONTROL B (gate G5): AN OMITTED FILL POINT ABORTS ON THAT VERB, AND ONLY THERE.
|
||||
//
|
||||
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
|
||||
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
|
||||
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
|
||||
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
|
||||
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
|
||||
// still copying the value, which is indistinguishable from a row that was never written.
|
||||
//
|
||||
// The scenario asserts both halves of "on THAT verb, and only there":
|
||||
//
|
||||
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
|
||||
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
|
||||
// mask, and the draw's own fields are stamped normally) and the
|
||||
// following glGenerateMipmap must abort naming exactly that pair.
|
||||
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
|
||||
// no Fatal at all. Without this half, "it aborted" would say nothing
|
||||
// about WHY: a poison that fired on every verb would look just as red.
|
||||
//
|
||||
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
|
||||
// entries, the second in the ambient Verify. entries (it skips when the knob IS set).
|
||||
//
|
||||
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
|
||||
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
|
||||
// sequence cannot run in the test process, and the harness's own bring-up pre-flight
|
||||
// (Harness/HeadlessGL.cpp) already establishes the shape: run it where a SIGABRT is a datum in
|
||||
// waitpid() instead of a dead lane. But that pre-flight forks BEFORE any context exists, and this case
|
||||
// cannot: the fixture has already brought one up. A bare fork() of a process holding a live Vulkan
|
||||
// device inherits the driver's mutexes with no threads to release them, and the child wedges on its
|
||||
// first submit - measured here as a 120s timeout on DirectVulkan and a clean pass on DirectGLES, which
|
||||
// is exactly the kind of backend-shaped flake a control must not have. So the child immediately
|
||||
// execve()s a fresh copy of this same test binary, filtered to the worker case below, which brings up
|
||||
// its own context from scratch and knows nothing about the parent's.
|
||||
//
|
||||
// The child gets its OWN MOBILEGL_LOG_FILE_PATH for the same reason: the library opens its log with
|
||||
// fopen(path, "w"), so a child sharing the parent's path would truncate the file the parent is about
|
||||
// to read.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||
#define MGITEST_POISON_HAVE_FORK 1
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
extern char** environ;
|
||||
#else
|
||||
#define MGITEST_POISON_HAVE_FORK 0
|
||||
#endif
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// What the PoisonOmitted. ctest entry and the CI negative-control step name. The pair is
|
||||
// spelled here so the assertion below is about the exact string the poison contracts to
|
||||
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
|
||||
constexpr const char* kOmittedVerb = "GenerateMipmap";
|
||||
constexpr const char* kOmittedField = "GetActiveTextureUnit";
|
||||
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
// Set only in the re-executed child, so the worker case below runs in that process and skips
|
||||
// everywhere else (including in the ambient lanes, where it is registered like any other case).
|
||||
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
|
||||
constexpr const char* kWorkerFilter =
|
||||
"--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// Where the child is told to write ITS log. Empty when the lane configured no log path at
|
||||
// all, in which case the signal is the only evidence and the text assertions are skipped.
|
||||
std::string ChildLogPath() {
|
||||
const std::filesystem::path parent = LibraryLogPath();
|
||||
if (parent.empty()) return {};
|
||||
return (parent.string() + ".poison-child");
|
||||
}
|
||||
|
||||
std::string ReadWholeFile(const std::string& path) {
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
class PoisonOmissionScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The sequence under test. Deliberately in this order: the DRAW comes first and must
|
||||
// survive - if the poison fired there, the "only that verb" half would be false and the
|
||||
// SIGABRT the parent waits for would prove nothing.
|
||||
void RunSequence() {
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// A two-level texture, so glGenerateMipmap has real work to do and cannot be
|
||||
// short-circuited into a no-op by a backend that inspects the level count first.
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
unsigned char pixels[8 * 8 * 4];
|
||||
for (std::size_t i = 0; i < sizeof(pixels); ++i) {
|
||||
pixels[i] = static_cast<unsigned char>(i);
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
|
||||
|
||||
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
|
||||
|
||||
// The sequence is the WHOLE datum this child reports, so a GL error in it must be
|
||||
// part of the answer rather than something only a human reading stderr would see.
|
||||
// WithoutOmissionCompletes reads the child's exit status, and the status is built
|
||||
// from HasFailure() below - so this EXPECT is what turns "the mipmap was rejected"
|
||||
// into a red parent instead of a vacuous "it exited 0, the poison did not fire".
|
||||
EXPECT_EQ(FirstGLError(), 0u)
|
||||
<< "the draw + glGenerateMipmap sequence the poison controls are about raised a "
|
||||
"GL error, so neither control is measuring what it claims to measure";
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// fork() + execve() of this same binary, filtered to the worker case, with the marker and
|
||||
// the child's own log path added to the environment. Everything that allocates happens
|
||||
// BEFORE the fork; between fork and execve only async-signal-safe work is done.
|
||||
static bool RunSequenceInAChildProcess(int& outStatus, std::string& outReason) {
|
||||
std::vector<std::string> env;
|
||||
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
|
||||
const std::string text(*entry);
|
||||
if (text.rfind("MOBILEGL_LOG_FILE_PATH=", 0) == 0) continue;
|
||||
if (text.rfind(std::string(kChildMarker) + "=", 0) == 0) continue;
|
||||
env.push_back(text);
|
||||
}
|
||||
env.push_back(std::string(kChildMarker) + "=1");
|
||||
const std::string childLog = ChildLogPath();
|
||||
if (!childLog.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(childLog, ec);
|
||||
env.push_back("MOBILEGL_LOG_FILE_PATH=" + childLog);
|
||||
}
|
||||
|
||||
std::vector<char*> envp;
|
||||
envp.reserve(env.size() + 1);
|
||||
for (std::string& entry : env) envp.push_back(entry.data());
|
||||
envp.push_back(nullptr);
|
||||
|
||||
std::string exe = "/proc/self/exe";
|
||||
std::string arg0 = "MobileGLIntegrationTest";
|
||||
std::string filter = kWorkerFilter;
|
||||
char* argv[] = {arg0.data(), filter.data(), nullptr};
|
||||
|
||||
std::fflush(nullptr);
|
||||
const pid_t child = fork();
|
||||
if (child < 0) {
|
||||
outReason = "fork() failed";
|
||||
return false;
|
||||
}
|
||||
if (child == 0) {
|
||||
execve(exe.c_str(), argv, envp.data());
|
||||
// execve only returns on failure; _exit, never exit(), because every atexit
|
||||
// handler in this address space belongs to the parent's copy of the world.
|
||||
std::fprintf(stderr, "[itest] poison child: execve(/proc/self/exe) failed\n");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
constexpr int kTimeoutMs = 120000;
|
||||
int waitedMs = 0;
|
||||
for (;;) {
|
||||
const pid_t reaped = waitpid(child, &outStatus, WNOHANG);
|
||||
if (reaped == child) return true;
|
||||
if (reaped < 0) {
|
||||
outReason = "waitpid on the poison worker failed";
|
||||
return false;
|
||||
}
|
||||
if (waitedMs >= kTimeoutMs) {
|
||||
kill(child, SIGKILL);
|
||||
(void)waitpid(child, &outStatus, 0);
|
||||
outReason = "the poison worker made no progress in 120s and was killed";
|
||||
return false;
|
||||
}
|
||||
timespec nap{0, 10 * 1000 * 1000};
|
||||
nanosleep(&nap, nullptr);
|
||||
waitedMs += 10;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string DescribeStatus(int status) {
|
||||
if (WIFEXITED(status)) return "exited with status " + std::to_string(WEXITSTATUS(status));
|
||||
if (WIFSIGNALED(status)) return "died on signal " + std::to_string(WTERMSIG(status));
|
||||
return "ended in an unrecognised way";
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
// The worker. It is a normal registered case so that the re-executed child can be selected
|
||||
// with nothing but --gtest_filter, and it skips in every process that is not that child.
|
||||
TEST_F(PoisonOmissionScenario, TheSequenceThePoisonControlsRun) {
|
||||
if (std::getenv(kChildMarker) == nullptr) {
|
||||
GTEST_SKIP() << "this case is the body the two poison controls run in a child process; "
|
||||
"it does nothing unless " << kChildMarker << " is set, which only the "
|
||||
"re-exec below does";
|
||||
}
|
||||
if (!Ready()) return;
|
||||
|
||||
RunSequence();
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// _exit, and not a return into gtest's teardown: this process exists to reach the verb
|
||||
// above and its exit status is the datum the parent reads. A normal teardown of a live
|
||||
// context could add signals of its own to that answer.
|
||||
//
|
||||
// HasFailure(), not 0: RunSequence() is full of ASSERT_/EXPECT_ macros, and a fatal one
|
||||
// (the shader failing to compile, say) RETURNS from RunSequence before the draw and the
|
||||
// glGenerateMipmap ever happen. Exiting 0 there would have WithoutOmissionCompletes pass
|
||||
// on a child that ran none of the sequence it is the control for - green because nothing
|
||||
// happened. The child's assertion text is on its stderr, which ctest captures.
|
||||
std::fflush(nullptr);
|
||||
_exit(::testing::Test::HasFailure() ? 1 : 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
|
||||
GTEST_SKIP() << "this case is the poison's negative control and needs "
|
||||
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
|
||||
"is what the PoisonOmitted. ctest entries set";
|
||||
}
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
|
||||
const std::string expectedPair = std::string(kOmittedField) + "@" + kOmittedVerb;
|
||||
if (knob != std::string(kOmittedVerb) + ":" + kOmittedField) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
|
||||
<< "how to provoke " << kOmittedVerb << ":" << kOmittedField;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
ASSERT_TRUE(WIFSIGNALED(status))
|
||||
<< "with the stamp of " << expectedPair << " omitted, the glGenerateMipmap in the child "
|
||||
<< "had to read a field its verb never filled and abort. It " << DescribeStatus(status)
|
||||
<< " instead - the poison is not armed (a build without MOBILEGL_PIPE_POISON, a filler "
|
||||
"that stamps what it was told to skip, or a backend that no longer reads the field "
|
||||
"through the accessor). Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WTERMSIG(status), SIGABRT)
|
||||
<< "the child died on signal " << WTERMSIG(status) << " rather than SIGABRT; the poison "
|
||||
"reports through MGLOG_F + std::abort(), so any other signal is a different crash. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
|
||||
if (ChildLogPath().empty()) {
|
||||
GTEST_SKIP() << "the abort happened, but the lane set no MOBILEGL_LOG_FILE_PATH, so the "
|
||||
"Fatal's text cannot be read back; the PoisonOmitted. ctest entries set it";
|
||||
}
|
||||
EXPECT_NE(childLog.find(std::string(kFatalPrefix) + ", \"" + expectedPair + "\""),
|
||||
std::string::npos)
|
||||
<< "the child aborted, but not with Fatal{UnmigratedPipeInput, \"" << expectedPair
|
||||
<< "\"} - that message is the whole diagnostic value of the poison. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("@DrawArrays"), std::string::npos)
|
||||
<< "the draw that ran BEFORE the omitted verb also tripped the poison, so the omission "
|
||||
"is not scoped to its verb: the fill classes are wrong, or the stamps are global. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
// The sibling control, in the ambient Verify. lanes: the same sequence with the knob UNSET
|
||||
// must run to completion and log no Fatal at all.
|
||||
//
|
||||
// It deliberately does NOT skip when MOBILEGL_PIPE_POISON_OMIT is set. This is the entry
|
||||
// CI's always-on negative control B exports the knob at: a green entry that the omission
|
||||
// turns red is the whole proof that the poison is armed, and an entry that politely skipped
|
||||
// itself would report that green either way. Nothing else in the integration suite calls
|
||||
// glGenerateMipmap, so this case is also the only possible target for that control.
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
|
||||
"build there is nothing for this control to be a control OF";
|
||||
}
|
||||
const bool omissionArmed = StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT");
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
const std::string note =
|
||||
omissionArmed
|
||||
? std::string(
|
||||
" NOTE: MOBILEGL_PIPE_POISON_OMIT is set in this process, so this failure is "
|
||||
"what CI's negative control B is asking for - the poison IS armed, and this "
|
||||
"entry going red is the proof.")
|
||||
: std::string();
|
||||
ASSERT_TRUE(WIFEXITED(status))
|
||||
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
|
||||
<< DescribeStatus(status)
|
||||
<< ". If it aborted, the poison is firing on a field the verb's fill table SHOULD list - "
|
||||
"add the row to MG_Pipe/FillPoints.def, never mark the field sticky."
|
||||
<< note << " Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WEXITSTATUS(status), 0)
|
||||
<< "the child " << DescribeStatus(status)
|
||||
<< ". Status 1 is the child's OWN assertion failing inside the sequence (it exits "
|
||||
"HasFailure() ? 1 : 0), so its gtest output on this job's stderr names the line; "
|
||||
"anything else came from the harness. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("Fatal{"), std::string::npos)
|
||||
<< "an unpoisoned run logged a Fatal:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,404 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitiveRestartScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - DESKTOP GL_PRIMITIVE_RESTART WITH AN APPLICATION-CHOSEN INDEX.
|
||||
//
|
||||
// Desktop GL restarts on whatever glPrimitiveRestartIndex named; GLES and Vulkan both restart
|
||||
// only on the all-ones value of the index type. DirectGLES used to THROW_EXCEPTION on the
|
||||
// mismatch, and a throw out of a GL entry point unwinds a C++ exception through the C ABI and
|
||||
// kills the process - which is how KHR-GL4x.geometry_shader.primitive_counter.*_rp took the whole
|
||||
// conformance runner down, nine bodies at a time, losing every result in the chunk with it.
|
||||
//
|
||||
// So the first thing this asserts is simply that the process is still here. The second is that
|
||||
// the restart actually happened: the substitution rewrites the index data so the driver restarts
|
||||
// where the application asked, and the difference between "restart honoured" and "restart
|
||||
// silently dropped" is a triangle strip that welds its two halves together across the gap.
|
||||
//
|
||||
// Needs a real context on purpose. The GPU-free suite cannot reach a backend at all, and this is
|
||||
// entirely about what the backend does with the index buffer.
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSurface = 64;
|
||||
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in vec2 a_position;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Two triangles with a gap down the middle, plus two spare vertices parked at the origin.
|
||||
//
|
||||
// The spares exist so the restart index is a LEGAL vertex index: if the restart were
|
||||
// dropped the driver would still fetch a real vertex rather than read out of bounds, so
|
||||
// the negative case is defined behaviour and the test measures the restart rather than
|
||||
// whatever robust-buffer-access does.
|
||||
constexpr GLfloat kVertices[] = {
|
||||
-0.9f, -0.9f, // 0 - left triangle
|
||||
-0.1f, -0.9f, // 1
|
||||
-0.9f, 0.9f, // 2
|
||||
0.1f, -0.9f, // 3 - right triangle
|
||||
0.9f, -0.9f, // 4
|
||||
0.9f, 0.9f, // 5
|
||||
0.0f, 0.0f, // 6 - spare
|
||||
0.0f, 0.0f, // 7 - spare, and the application's restart index
|
||||
};
|
||||
constexpr GLuint kRestartIndex = 7;
|
||||
|
||||
// A triangle STRIP, restarted in the middle: honoured, it is exactly the two triangles
|
||||
// above. Dropped, the strip welds vertices 2, 7 and 3 into extra triangles that spill
|
||||
// across the gap - which is what the middle probe below catches.
|
||||
constexpr GLuint kIndices[] = {0, 1, 2, kRestartIndex, 3, 4, 5};
|
||||
|
||||
struct Pixel {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
};
|
||||
|
||||
class PrimitiveRestartScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glGenBuffers(1, &m_ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
|
||||
glGenTextures(1, &m_colorTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_colorTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSurface, kSurface);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
|
||||
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSurface, kSurface);
|
||||
|
||||
m_program = BuildProgram();
|
||||
ASSERT_NE(m_program, 0u) << "the flat-colour program did not build: " << m_buildLog;
|
||||
glUseProgram(m_program);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
glPrimitiveRestartIndex(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_colorTexture != 0) glDeleteTextures(1, &m_colorTexture);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// The whole surface, so a failure can report the three probes together rather than
|
||||
// three separate readbacks that might disagree about which draw they saw.
|
||||
std::vector<Pixel> DrawAndRead() {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(kIndices)), GL_UNSIGNED_INT,
|
||||
nullptr);
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
static const Pixel& At(const std::vector<Pixel>& pixels, int x, int y) {
|
||||
return pixels[static_cast<std::size_t>(y) * kSurface + x];
|
||||
}
|
||||
|
||||
static bool IsGreen(const Pixel& p) { return p.g > 128 && p.r < 128; }
|
||||
|
||||
// NDC (-0.5, -0.5): well inside the left triangle whichever way the restart went.
|
||||
static constexpr int kLeftX = 16, kLeftY = 16;
|
||||
// NDC (0.6, -0.5): well inside the right triangle, and outside every welded one.
|
||||
static constexpr int kRightX = 51, kRightY = 16;
|
||||
// NDC (0.2, -0.5): in the gap between the two triangles, and INSIDE the triangle the
|
||||
// strip welds out of vertices 7, 3 and 4 when the restart is dropped. This is the
|
||||
// probe that distinguishes a working restart from a silently ignored one.
|
||||
static constexpr int kGapX = 38, kGapY = 16;
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_colorTexture = 0;
|
||||
GLuint m_program = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// THE crash regression. Before the fix this call never returned: DirectGLES threw
|
||||
// std::runtime_error out of glDrawElements and the process died on the spot. Reaching the
|
||||
// assertion at all is most of the point.
|
||||
TEST_F(PrimitiveRestartScenario, AnArbitraryRestartIndexDrawsInsteadOfKillingTheProcess) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "an arbitrary restart index is legal desktop GL and must raise no error";
|
||||
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the gap between the two halves is covered, so the restart was dropped and the "
|
||||
"strip welded across it";
|
||||
}
|
||||
|
||||
// The other half of the state: an application that sets the restart index TO the fixed
|
||||
// all-ones value needs no rewriting at all, and the cap must map straight onto the
|
||||
// driver's own fixed-index restart. Same picture, different path through the backend.
|
||||
TEST_F(PrimitiveRestartScenario, TheFixedIndexValueTakesTheForwardingPath) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// Index 0xFFFFFFFF is not a vertex this draw uses, so the strip is the same shape.
|
||||
const GLuint fixedIndices[] = {0, 1, 2, 0xFFFFFFFFu, 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(fixedIndices), fixedIndices);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0xFFFFFFFFu);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY)));
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)));
|
||||
|
||||
// Put the buffer back for whatever runs next in this fixture.
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(kIndices), kIndices);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// With the cap off, the same index data is just data - nothing restarts, and the strip
|
||||
// welds across the gap. The negative control for the probe above: without it, a backend
|
||||
// that lost the whole draw would pass the test by rendering nothing in the gap.
|
||||
TEST_F(PrimitiveRestartScenario, WithoutTheCapTheStripWeldsAcrossTheGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the draw itself must still happen";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "with restart disabled the strip is continuous, so the gap must be covered - if "
|
||||
"it is not, the probe above proves nothing";
|
||||
}
|
||||
|
||||
// A second draw with a DIFFERENT restart index has to be rewritten again. The substitution
|
||||
// stages through one scratch buffer, so a cached or half-restored element-array binding
|
||||
// would show up here as the second draw reusing the first one's data.
|
||||
TEST_F(PrimitiveRestartScenario, ChangingTheRestartIndexBetweenDrawsIsHonoured) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
const std::vector<Pixel> restarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FALSE(IsGreen(At(restarted, kGapX, kGapY)));
|
||||
|
||||
// 6 is the other spare vertex, and it appears nowhere in the index data - so nothing
|
||||
// restarts and the strip is continuous again, from the very same buffer.
|
||||
glPrimitiveRestartIndex(6);
|
||||
const std::vector<Pixel> notRestarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kGapX, kGapY)))
|
||||
<< "the second draw restarted on an index that is not in its data";
|
||||
}
|
||||
|
||||
// A NON-indexed draw has no index stream, so GL primitive restart cannot affect it - and a
|
||||
// list topology is the shape DirectVulkan has to refuse when the device lacks
|
||||
// VK_EXT_primitive_topology_list_restart. Deriving the pipeline's primitiveRestartEnable
|
||||
// from the capability bits alone conflated the two: an application that enables
|
||||
// GL_PRIMITIVE_RESTART once at init and then draws its UI with glDrawArrays(GL_TRIANGLES)
|
||||
// had every one of those draws silently dropped on such a device.
|
||||
TEST_F(PrimitiveRestartScenario, ANonIndexedListTopologyDrawIsUnaffectedByTheCap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Vertices 0,1,2 are the left triangle; GL_TRIANGLES is a list topology.
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)))
|
||||
<< "primitive restart has no meaning for glDrawArrays, so the draw must render "
|
||||
"normally whatever the device supports";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.6 compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX. A restart index the index type cannot hold therefore matches
|
||||
// nothing and the draw restarts NOWHERE - it does not restart on the truncated value, and
|
||||
// it does not restart on the type's all-ones value either, which is what the driver's own
|
||||
// fixed-index restart would have done if it had been left enabled.
|
||||
TEST_F(PrimitiveRestartScenario, ARestartIndexTooLargeForTheIndexTypeRestartsNowhere) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// 16-bit indices with a restart index of 0x10007: the low half (7) IS a real index in
|
||||
// the data, so a truncating comparison would split the strip exactly where a correct
|
||||
// one leaves it whole.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0x10000u + kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(shortIndices)), GL_UNSIGNED_SHORT,
|
||||
nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "no 16-bit index can equal 0x10007, so nothing restarts and the strip is "
|
||||
"continuous - truncating the restart index to 7 would split it here";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The all-ones value of an index type is an ordinary vertex index whenever the array uses
|
||||
// the type's full range, which is exactly why an application picks an arbitrary restart
|
||||
// index in the first place. Substituting the sentinel in place would either steal that
|
||||
// vertex or spuriously restart on it, so the copy widens instead - and the draw has to be
|
||||
// issued with the widened type, which is the part that is easy to forget.
|
||||
TEST_F(PrimitiveRestartScenario, AnAllOnesVertexIndexSurvivesTheSubstitution) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// The buffer carries the 16-bit all-ones value as an ordinary element. It sits past
|
||||
// the seven indices this draw reads, because the vertex array has only eight entries
|
||||
// and fetching index 65535 would be out of range - what is under test is that its
|
||||
// mere PRESENCE forces the widened copy, and that the draw still finds its own
|
||||
// indices at the right offsets in a copy whose element width has changed underneath
|
||||
// it. Narrowly substituting in place instead would rewrite this element to 0xFFFE.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5, 0xFFFFu};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Only the first seven indices are drawn, so the 0xFFFF element is never fetched - what
|
||||
// is under test is that its PRESENCE does not break the substitution or the offsets.
|
||||
glDrawElements(GL_TRIANGLE_STRIP, 7, GL_UNSIGNED_SHORT, nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the restart still has to happen once the copy has been widened";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,554 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GL_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
|
||||
// INACTIVE.
|
||||
//
|
||||
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
|
||||
// capture or no capture. The CTS leans its whole tessellation suite on that - the
|
||||
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
|
||||
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
|
||||
// tessellation tests are sized from the answer - so a backend that answers 0
|
||||
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
|
||||
//
|
||||
// DirectVulkan serves the query from the transform-feedback stream query's
|
||||
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
|
||||
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
|
||||
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
|
||||
// the pair reads back 0. Where the bring-up probe measures that defect with a
|
||||
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
|
||||
// renderer accumulates XFB-inactive draws through the best proven substitute
|
||||
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
|
||||
// rasterizer discard included), else pipeline statistics over clipping-stage
|
||||
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
|
||||
// answer, so on this machine they hold the reroute to the same numbers the
|
||||
// healthy stream path must produce - the "two pools must agree" assertion - and
|
||||
// on a healthy driver they pin the stream path itself.
|
||||
//
|
||||
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
|
||||
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
|
||||
// is a different mechanism with its own tests.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
|
||||
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
|
||||
// same program and simply never begin a span.
|
||||
const char* const kVertexSource = R"(#version 430 core
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vs_out_value = vec4(1.0);
|
||||
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
|
||||
// triangle per patch - the count the tessellation cases assert.
|
||||
const char* const kTessVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* const kTessControlSource = R"(#version 430 core
|
||||
layout(vertices = 1) out;
|
||||
void main() {
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
const char* const kTessEvalSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
void main() {
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The same tessellation pipeline with something to capture, so that
|
||||
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
|
||||
// open (but paused) capture span AND a tessellator in one program.
|
||||
const char* const kTessEvalCaptureSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
out vec4 te_out_value;
|
||||
void main() {
|
||||
te_out_value = vec4(1.0);
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != std::string("DirectVulkan")) {
|
||||
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
|
||||
<< Gl().BackendName()
|
||||
<< " answers this query from a different mechanism";
|
||||
}
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenQueries(2, m_queries);
|
||||
ASSERT_NE(m_queries[0], 0u);
|
||||
ASSERT_NE(m_queries[1], 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
|
||||
m_queries[0] = m_queries[1] = 0;
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// captureVarying: the name to record with glTransformFeedbackVaryings, or
|
||||
// nullptr for a program that can never open a capture span.
|
||||
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||
const char* captureVarying) {
|
||||
std::vector<GLuint> shaders;
|
||||
for (const auto& [type, source] : stages) {
|
||||
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||
if (shader == 0) {
|
||||
for (const GLuint built : shaders) glDeleteShader(built);
|
||||
return 0;
|
||||
}
|
||||
shaders.push_back(shader);
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (captureVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram() {
|
||||
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
|
||||
}
|
||||
|
||||
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTessGenLevel < 1) return 0;
|
||||
return BuildProgram(
|
||||
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER,
|
||||
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
|
||||
withCaptureVarying ? "te_out_value" : nullptr);
|
||||
}
|
||||
|
||||
// A capture span that is open but PAUSED. The pause closes the capture, so
|
||||
// every draw inside it is XFB-inactive at the backend - the stream query's
|
||||
// silent case - while the GL span stays active. `program` must be the one
|
||||
// that is bound: GL requires the same program at resume.
|
||||
void BeginPausedSpan() {
|
||||
glGenBuffers(1, &m_captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glPauseTransformFeedback();
|
||||
}
|
||||
|
||||
void EndPausedSpan() {
|
||||
glResumeTransformFeedback();
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
|
||||
m_captureBuffer = 0;
|
||||
}
|
||||
|
||||
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||
GLuint QueryGenerated(const std::function<void()>& record) {
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
||||
record();
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint generated = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
static GLenum DrainGLErrors() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||
GLuint m_captureBuffer = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// The plain shape: no capture object was ever bound, no span begun, no
|
||||
// rasterizer discard - just a GENERATED query around two triangles. On a
|
||||
// healthy driver the stream query answers it; on an affected one the armed
|
||||
// reroute must produce the same 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
|
||||
"inactive (GL 4.6 core 13.4)";
|
||||
}
|
||||
|
||||
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
|
||||
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
|
||||
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
|
||||
// tests' capture buffers on the affected device.
|
||||
//
|
||||
// On lavapipe this case holds through the dedicated
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
|
||||
// what makes a discarded draw countable there - llvmpipe's clipping
|
||||
// statistics AND stream query both read 0 under discard).
|
||||
//
|
||||
// The value-conditioned skip below is deliberate and narrow, for a stack
|
||||
// with NO counter that survives discard: there this case is unfalsifiable,
|
||||
// and a red would indict MobileGL for a hole the bring-up probe already
|
||||
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
|
||||
// exact-zero answer IS the capability signal - any wrong nonzero count
|
||||
// still fails - and on every driver that counts discarded draws at all the
|
||||
// full assertion runs. The device probe list holds this shape on the Mali.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (generated == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
|
||||
"primitives-generated query, clipping statistics) survives "
|
||||
"rasterizer discard for an XFB-inactive draw on this stack - the "
|
||||
"shape is unfalsifiable here; the bring-up probe measures the same "
|
||||
"hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "rasterizer discard drops primitives after clipping and must not hide them from "
|
||||
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
|
||||
"tessellator with";
|
||||
}
|
||||
|
||||
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
|
||||
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
|
||||
// protocol derives everything from. Undiscarded, so that the answer is
|
||||
// holdable on this machine through whichever accounting path is armed (the
|
||||
// discard interaction is the case above's business, measured separately).
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram();
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u)
|
||||
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
|
||||
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
|
||||
}
|
||||
|
||||
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
|
||||
// captured one, then another XFB-inactive one. The GENERATED answer must
|
||||
// accumulate across the two accounting paths the armed reroute splits them
|
||||
// into (stream slots for the captured draw, statistics slots for the
|
||||
// others), and WRITTEN must stay exactly the captured draw's count - the
|
||||
// pairing the stream path exists to keep exact. Undiscarded, so the
|
||||
// accumulation invariant is holdable on this machine (see the discard
|
||||
// case's comment); the triangles rasterize into the harness framebuffer,
|
||||
// which nothing here reads.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
||||
const GLuint generated = QueryGenerated([]() {
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
|
||||
glEndTransformFeedback();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
|
||||
});
|
||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||
|
||||
GLuint written = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
|
||||
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
||||
}
|
||||
|
||||
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
|
||||
//
|
||||
// glPauseTransformFeedback closes the capture without closing the span, so a
|
||||
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
|
||||
// exactly as silent for it as for a draw with no span at all - while
|
||||
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
|
||||
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
|
||||
// silences). The frontend does keep a CPU counter for paused draws, but it can
|
||||
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
|
||||
// these draws are the reroute's business like any other - and the trap on the
|
||||
// other side is counting them TWICE, once in each accounting.
|
||||
//
|
||||
// Each case measures the SAME draw twice: once with no span open at all (the
|
||||
// capability control - what this stack can count) and once inside the paused
|
||||
// span, and requires the two to agree. That differential is what makes these
|
||||
// cases falsifying rather than vacuous: a stack where no counter reaches a
|
||||
// capture-less draw fails the control and skips, while a stack that counts the
|
||||
// unpaused draw and answers 0 for the paused one - which is what excluding
|
||||
// paused draws from the reroute produced - fails, instead of skipping into
|
||||
// green.
|
||||
|
||||
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
|
||||
// CPU delta, this reads 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
|
||||
"draws are CPU-counted on top of whatever the stream query says";
|
||||
}
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half of the comparison proves nothing; the "
|
||||
"bring-up probe measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "one triangle drawn while the capture span is paused is still one primitive "
|
||||
"generated - counted once, by whichever accounting owns it, never by two of them "
|
||||
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
|
||||
}
|
||||
|
||||
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
|
||||
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
|
||||
// the CTS's tessellator-measuring shape. Excluding paused draws from the
|
||||
// reroute left this counted by nothing at all on the affected device.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
|
||||
"on this stack, so the paused half proves nothing; the bring-up probe "
|
||||
"measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u)
|
||||
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
|
||||
"exactly one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "pausing the capture does not stop the tessellator from generating that triangle, "
|
||||
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
|
||||
"patch draw left out of the reroute is counted by nothing at all";
|
||||
}
|
||||
|
||||
// The other half of the same hole: the instanced entry points never reach the
|
||||
// frontend's paused accounting either, so a paused instanced draw excluded from
|
||||
// the reroute is likewise counted by nothing.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused =
|
||||
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half proves nothing";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "four instances generate four primitives whether or not the capture span is "
|
||||
"paused, and no instanced entry point reaches the frontend's paused accounting";
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||
// is green here whether the reroute ran or not (that is the "two pools
|
||||
// agree" point), so none of them can say the pinned lane actually exercised
|
||||
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
|
||||
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
|
||||
// XFB-inactive draw inside a GENERATED span must make the renderer say -
|
||||
// through its latched MGLOG_I - that it engaged the reroute. It reads
|
||||
// MG_Config not at all (on Android this module links the shipping library)
|
||||
// and trusts only the log bytes appended after it started.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
|
||||
"is what the PrimGenReroute. ctest entry does with "
|
||||
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
|
||||
"decides and this machine's verdict is its own business";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it rerouted anything; the PrimGenReroute. ctest "
|
||||
"entry sets both";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
// Taken BEFORE the draw, so the line this looks for can only be one this
|
||||
// process wrote for this span. The latch fires on the FIRST rerouted
|
||||
// draw, which is inside the query below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
|
||||
"a GENERATED query, and the renderer never reported engaging the reroute. The "
|
||||
"quirk is not armed - check the override mapping "
|
||||
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
|
||||
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,227 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - BLENDING WORKS ON A RENDERBUFFER WHOSE GL FORMAT HAS NO EXACT VkFormat.
|
||||
//
|
||||
// DirectVulkan force-disables blending on an attachment whose VkFormat lacks
|
||||
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT, which is the right thing to do - blending on such a
|
||||
// format is invalid pipeline state. The probe has to ask about the format the attachment ACTUALLY
|
||||
// has, and for renderbuffers it asked a different question from the one that created the image: the
|
||||
// image comes from ResolveTextureFormatInfo (which widens GL formats with no Vulkan twin onto a real
|
||||
// one) while the probe used the strict 1:1 converter, which answers VK_FORMAT_UNDEFINED for RGBA2,
|
||||
// RGBA12, RGB10, RGB12, RGB16 and the three-channel formats, and the 16-bit packed formats for RGBA4
|
||||
// and RGB5_A1.
|
||||
//
|
||||
// VkFormatProperties for VK_FORMAT_UNDEFINED are all zero, so the probe concluded "not blendable"
|
||||
// and every pipeline for that attachment was built with blendEnable = VK_FALSE - permanently, and
|
||||
// silently apart from one log line. The source colour then overwrites the destination instead of
|
||||
// blending with it, which is a wrong PICTURE, not a wrong error code.
|
||||
//
|
||||
// GL_RGB8 is the ordinary shape and is what this scenario leads with: it is a required
|
||||
// colour-renderable format, its image has been R8G8B8A8_UNORM all along, and the probe asked about
|
||||
// the 24-bit R8G8B8_UNORM that most drivers do not support at all. GL_RGBA4 covers the other half -
|
||||
// a format whose probe answered a real-but-different VkFormat.
|
||||
//
|
||||
// DirectGLES is the control: it forwards the renderbuffer to the ES driver and blends whatever the
|
||||
// driver blends, so a disagreement between the two backends is the defect.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = uColor;
|
||||
}
|
||||
)";
|
||||
|
||||
class RenderbufferBlendFormatScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << "program did not build: " << error;
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
Destroy();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns false (having skipped, not failed) when the driver will not give us a complete
|
||||
// framebuffer for this format - GL only requires a subset of formats to be
|
||||
// colour-renderable, and the point of the scenario is blending, not format support.
|
||||
bool MakeTarget(GLenum internalFormat) {
|
||||
Destroy();
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
return status == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void DrawColor(float r, float g, float b, float a) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uColor"), r, g, b, a);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// One draw of opaque black, then a 50%-alpha white draw over it with the ordinary
|
||||
// SRC_ALPHA / ONE_MINUS_SRC_ALPHA function. Blending gives mid-grey; a pipeline built with
|
||||
// blendEnable = VK_FALSE gives white, because the source simply overwrites.
|
||||
//
|
||||
// The tolerance is wide on purpose: RGBA4 has four bits per channel, so "mid-grey" is one of
|
||||
// a handful of representable values and the test must not become a quantisation test.
|
||||
void ExpectBlendedRatherThanOverwritten(const char* what) {
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty()) << what;
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(int(centre.r), 40) << what << ": got " << centre << ", which is darker than a blend of "
|
||||
"black and 50% white";
|
||||
EXPECT_LT(int(centre.r), 215) << what << ": got " << centre
|
||||
<< ", which is the source colour - blending was disabled";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The ordinary case, and the one broken today rather than only after the format table was
|
||||
// unified: a three-channel colour renderbuffer. Its image has been R8G8B8A8_UNORM all along while
|
||||
// the blend probe asked about R8G8B8_UNORM, which most drivers do not support at all.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAThreeChannelRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGB8)) GTEST_SKIP() << "GL_RGB8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGB8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The other half: a format whose strict converter answers a real-but-different VkFormat
|
||||
// (R4G4B4A4_UNORM_PACK16) while the image is R8G8B8A8_UNORM. Blend support for the packed 16-bit
|
||||
// formats is optional in Vulkan, so the probe could legitimately answer "no" for a format the
|
||||
// attachment does not have.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnALowBitPackedRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA4)) GTEST_SKIP() << "GL_RGBA4 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA4");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The control that keeps both of the above honest: the same sequence on the format whose probe
|
||||
// and image always agreed. If this one ever fails, the scenario is measuring the blend setup
|
||||
// rather than the format resolution.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAnRgba8Renderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA8)) GTEST_SKIP() << "GL_RGBA8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,178 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
|
||||
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
|
||||
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
|
||||
// glSampleMaski last wrote.
|
||||
//
|
||||
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
|
||||
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
|
||||
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
|
||||
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
|
||||
// a fully black draw:
|
||||
//
|
||||
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
|
||||
// ... render ...
|
||||
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
|
||||
//
|
||||
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
|
||||
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
|
||||
// shape every application uses.
|
||||
//
|
||||
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
|
||||
// one asserts that the mask changed nothing.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kGreenFragmentSource = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampleMaskScopeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
// Process-wide GL state: leaving it set would hand the next scenario in this
|
||||
// process the very bug under test.
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
void ExpectQuadStillPaints(const char* what) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
|
||||
<< what << ": an all-black target means the sample mask discarded every fragment, "
|
||||
<< "which GL says it cannot do on a single-sample framebuffer";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
|
||||
// masked off if the mask is applied at all.
|
||||
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
|
||||
}
|
||||
|
||||
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
|
||||
// set.
|
||||
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
|
||||
}
|
||||
|
||||
// Control: the same mask word with the capability disabled has never had any effect, so this
|
||||
// one passed before the fix too. It is here so a regression that ignores the enable bit
|
||||
// instead of the sample count is still caught.
|
||||
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
|
||||
}
|
||||
|
||||
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
|
||||
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
|
||||
// mask change, both to the same single-sample target, both required to paint.
|
||||
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
ExpectQuadStillPaints("first draw, full mask");
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("second draw, mask 0x2");
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
|
||||
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
|
||||
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
|
||||
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
|
||||
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
GLint words = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
|
||||
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
|
||||
for (GLint word = 0; word < words; ++word) {
|
||||
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
|
||||
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,274 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleVariablesScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - gl_NumSamples REACHES THE SHADER, AND IT FOLLOWS THE DRAW FRAMEBUFFER.
|
||||
//
|
||||
// glslang declares gl_NumSamples only when it is NOT targeting SPIR-V - both the desktop and the
|
||||
// ES branch of Initialize.cpp wrap `uniform int gl_NumSamples;` in `if (spvVersion.spv == 0)`,
|
||||
// because SPIR-V has no NumSamples builtin to lower it to - and MobileGL always targets SPIR-V.
|
||||
// Every fragment shader that read the built-in therefore died at COMPILE time with
|
||||
// "'gl_NumSamples' : undeclared identifier", which is all 144 KHR-GL46.sample_variables.mask.*
|
||||
// bodies plus their es_31_compatibility twins.
|
||||
//
|
||||
// The source pipeline now lowers it onto a reserved default-block uniform and the draw path writes
|
||||
// the current draw framebuffer's sample count into it. Two claims, and the second is the one a
|
||||
// compile-only test cannot make: the value must be the DRAW FRAMEBUFFER's, so one program drawn
|
||||
// into a multisample target and then into a single-sample target has to report both counts. A
|
||||
// link-time bake would pass the first assertion and fail the second, which is exactly why the
|
||||
// write lives per draw.
|
||||
//
|
||||
// llvmpipe and lavapipe both offer 4x multisample RGBA8, so this runs for real in CI rather than
|
||||
// skipping; the skips below are for a driver that offers no multisample renderbuffer at all.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVS = R"(#version 400 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// gl_NumSamples scaled so each count lands on its own well-separated 8-bit value: 1 -> 16,
|
||||
// 2 -> 32, 4 -> 64. Every sample of the fragment gets the same colour, so the resolve blit
|
||||
// averages identical values and the readback is exact rather than approximate.
|
||||
constexpr const char* kFS = R"(#version 400 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(float(gl_NumSamples) * (16.0 / 255.0), 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class SampleVariablesScenario : public ScenarioTest {};
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SampleVariablesScenario, GlNumSamplesFollowsTheDrawFramebuffersSampleCount) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
// The compile failure this scenario exists for lands here, with glslang's own text.
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// ---- multisample target ----
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
// What the driver actually allocated - a request is a lower bound, and the shader has to
|
||||
// agree with the query rather than with what was asked for.
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
ASSERT_GE(realizedSamples, 2) << "the multisample framebuffer reports GL_SAMPLES " << realizedSamples;
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Resolve into the default framebuffer to read it back.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0) << " into a " << realizedSamples
|
||||
<< "-sample framebuffer; 1 means the reserved uniform was never written, 0 means it was "
|
||||
<< "written but never uploaded";
|
||||
}
|
||||
gl.EndFrame();
|
||||
|
||||
// ---- the SAME program into a single-sample target ----
|
||||
// A link-time bake of the sample count would keep reporting the multisample value here.
|
||||
GLuint ssFbo = 0, ssRbo = 0;
|
||||
glGenFramebuffers(1, &ssFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, ssFbo);
|
||||
glGenRenderbuffers(1, &ssRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, ssRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ssRbo);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image single = ReadPixels(width, height);
|
||||
const Rgba8 centre = single.At(width / 2, height / 2);
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is ONE for a non-multisample framebuffer, where
|
||||
// glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
EXPECT_NEAR(centre.r, 16, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0)
|
||||
<< " into a single-sample framebuffer; the value is a property of the DRAW FRAMEBUFFER, "
|
||||
<< "so re-using the program must re-write it";
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteRenderbuffers(1, &ssRbo);
|
||||
glDeleteFramebuffers(1, &ssFbo);
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// ARB_sample_shading is advertised, and until now glMinSampleShading was a logging no-op while
|
||||
// glEnable(GL_SAMPLE_SHADING) fell out of RenderState::SetCapability's default arm - so an
|
||||
// application could ask for a shading rate and get silence from both halves.
|
||||
//
|
||||
// What this can and cannot assert. The RATE itself is not observable from a portable shader:
|
||||
// GL 4.6 core 14.3.1 makes any use of gl_SampleID or gl_SamplePosition force per-sample
|
||||
// evaluation on its own, so the very built-ins that would report the rate defeat the
|
||||
// measurement. What IS worth pinning is that the state now reaches both backends without
|
||||
// damage: DirectGLES forwards glEnable(GL_SAMPLE_SHADING) + glMinSampleShading to the ES
|
||||
// driver (and must not, on a driver that has neither, push an INVALID_ENUM into the
|
||||
// application's error queue), and DirectVulkan bakes sampleShadingEnable/minSampleShading into
|
||||
// a NEW pipeline - which it may only do with the device's sampleRateShading feature enabled.
|
||||
TEST_F(SampleVariablesScenario, SampleShadingStateReachesTheBackendWithoutDisturbingTheDraw) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; sample shading needs a multisample target";
|
||||
}
|
||||
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glViewport(0, 0, width, height);
|
||||
|
||||
glEnable(GL_SAMPLE_SHADING);
|
||||
glMinSampleShading(1.0f);
|
||||
EXPECT_EQ(glIsEnabled(GL_SAMPLE_SHADING), static_cast<GLboolean>(GL_TRUE));
|
||||
GLfloat rate = -1.0f;
|
||||
glGetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &rate);
|
||||
EXPECT_FLOAT_EQ(rate, 1.0f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "enabling sample shading raised a GL error";
|
||||
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sample-shading draw raised a GL error";
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
// The rate changes how OFTEN the shader runs, never what it computes - so the same
|
||||
// gl_NumSamples reading has to come back.
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "the draw changed its result once sample shading was enabled";
|
||||
|
||||
glMinSampleShading(0.0f);
|
||||
glDisable(GL_SAMPLE_SHADING);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,231 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampledSetStalenessScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A TEXTURE THAT BECOMES COMPLETE WITHOUT A REBIND MUST RE-ENTER THE SAMPLED SET.
|
||||
//
|
||||
// DirectVulkan does not bind a texture GL calls incomplete: it substitutes a fallback so the
|
||||
// sampler reads (0,0,0,1) instead of losing the draw. That decision is made twice per draw - once
|
||||
// by CollectSampledTextures, which builds the list SetupDraw syncs, materialises pending clears
|
||||
// for and transitions to a sampled layout BEFORE the render pass opens, and once by the descriptor
|
||||
// resolve inside the pass. Both ask SamplesAsIncompleteTexture.
|
||||
//
|
||||
// The per-draw memo that lets the first of those be skipped was keyed only on the program, the
|
||||
// transform flags and the texture BIND generation. Completeness is not a function of any of them:
|
||||
// it moves on a filter change (glTexParameteri / glSamplerParameteri), on a level-range change,
|
||||
// and on an upload that fills the mip chain - none of which bind anything. So a texture that went
|
||||
// incomplete -> complete under a fixed binding kept being answered out of the memo as "not in the
|
||||
// set", and the work SetupDraw does for the set never happened for it:
|
||||
//
|
||||
// * its queued clear was never materialised, so the draw sampled pre-clear content - wrong
|
||||
// pixels, no validation layer needed, which is what the case below detects; and
|
||||
// * its layout transition moved into the descriptor resolve, which records
|
||||
// vkCmdPipelineBarrier inside an already-open render pass whose subpass declares no
|
||||
// self-dependency - the exact hazard CollectSampledTextures exists to prevent.
|
||||
//
|
||||
// The fix adds the sampling-resolution generation to that memo key, which is the counter the
|
||||
// codebase already maintains for "what a unit resolves to changed without a bind" and which both
|
||||
// TextureObjectBase::BumpShapeVersion and SamplerObject::BumpVersion move.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
constexpr int kTexSize = 8;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// texelFetch, not texture(): the point is WHICH image is sampled, and a fetch cannot be
|
||||
// explained away by filtering.
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_tex;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = texelFetch(u_tex, ivec2(0, 0), 0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampledSetStalenessScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
// The sampled texture: ONE level, and no glTexParameteri at all, so MIN_FILTER
|
||||
// keeps its initial GL_NEAREST_MIPMAP_LINEAR and GL calls it mipmap-incomplete.
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
std::vector<unsigned char> green(static_cast<std::size_t>(kTexSize * kTexSize * 4), 0);
|
||||
for (std::size_t i = 0; i < green.size(); i += 4) {
|
||||
green[i + 1] = 255;
|
||||
green[i + 3] = 255;
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
green.data());
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// One draw of the fullscreen quad sampling texel (0,0) of whatever unit 0 holds, and
|
||||
// NO readback. That matters: a readback submits and waits, which ends the command
|
||||
// buffer and resets the per-draw memos with it - so a case that read back between its
|
||||
// two draws would never leave a stale entry to catch. The two draws here have to land
|
||||
// in one recording.
|
||||
void DrawOnly() {
|
||||
BindFbo(m_target);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const GLint location = glGetUniformLocation(m_program, "u_tex");
|
||||
if (location != -1) glUniform1i(location, 0);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw raised a GL error";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_texture = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The full sequence, ordered so the ONLY state change between the two draws is the filter.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenAFilterChangeCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// 1. Queue a clear on the texture through an FBO and take it straight back out, with no
|
||||
// draw in between - the "attach -> clear -> detach" shape that leaves the clear
|
||||
// pending for whoever samples the texture next.
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
|
||||
// 2. Draw while the texture is still incomplete. The backend substitutes its fallback,
|
||||
// and the per-draw memo records the resulting sampled set.
|
||||
DrawOnly();
|
||||
|
||||
// 3. Make it complete. No bind, no upload, no program change - one filter write, which is
|
||||
// exactly the state the old memo key could not see.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the filter raised a GL error";
|
||||
|
||||
// 4. Draw again, into the same recording, and only now read back. The texture is in the
|
||||
// sampled set now, so its queued clear has to be materialised before the pass opens and
|
||||
// the fetch has to see RED. Reading the green the texture was uploaded with means the
|
||||
// clear was never materialised, i.e. the texture never entered the set - the stale-memo
|
||||
// bug. Black means the fallback was still being handed out.
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the completeness flip"))
|
||||
<< "green means the queued clear was never materialised, so the texture never re-entered "
|
||||
"the sampled set after the filter change; blue means the draw did not happen at all";
|
||||
}
|
||||
|
||||
// The same flip driven from a SAMPLER OBJECT rather than the texture's own parameters. It is
|
||||
// the other half of what feeds the completeness predicate, it moves the same generation, and
|
||||
// it likewise binds nothing.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenASamplerObjectCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint sampler = 0;
|
||||
glGenSamplers(1, &sampler);
|
||||
// Bound BEFORE the first draw, still carrying the mipmapping default, so binding it is
|
||||
// not what changes between the two draws.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glBindSampler(0, sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object raised a GL error";
|
||||
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawOnly();
|
||||
|
||||
// One parameter write on an ALREADY-BOUND sampler object.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sampler filter raised a GL error";
|
||||
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the sampler-object flip"))
|
||||
<< "green means the queued clear was never materialised after the sampler parameter change";
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &sampler);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,339 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END.
|
||||
//
|
||||
// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and
|
||||
// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments,
|
||||
// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM.
|
||||
// Every gl_spirv conformance body died on the first of those two calls.
|
||||
//
|
||||
// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two
|
||||
// modules that glslang compiled ahead of time (embedded below as words, so the test depends on
|
||||
// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel
|
||||
// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe.
|
||||
//
|
||||
// The two specialization constants are the load-bearing part. The vertex module scales its
|
||||
// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant
|
||||
// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a
|
||||
// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of
|
||||
// green), and BOTH would show up in the readback. A "specialization" that merely stored the
|
||||
// values without folding them in is exactly the failure mode this shape is built to catch.
|
||||
//
|
||||
// The GLSL the modules came from:
|
||||
// vertex: layout(location = 0) in vec2 aPos;
|
||||
// layout(constant_id = 3) const float uScale = 1.0;
|
||||
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
|
||||
// fragment: layout(location = 0) out vec4 oColor;
|
||||
// layout(constant_id = 7) const int uChannel = 0;
|
||||
// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; }
|
||||
// compiled with `glslangValidator -G --target-env opengl`.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V
|
||||
#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551
|
||||
#endif
|
||||
#ifndef GL_SPIR_V_BINARY
|
||||
#define GL_SPIR_V_BINARY 0x9552
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
class SpirvShaderBinaryScenario : public ScenarioTest {};
|
||||
|
||||
// 255 words
|
||||
const unsigned int kVertexModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
|
||||
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
|
||||
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
|
||||
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
|
||||
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
|
||||
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
|
||||
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
|
||||
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
|
||||
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
|
||||
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
|
||||
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
|
||||
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
|
||||
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
|
||||
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
|
||||
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
|
||||
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
|
||||
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
|
||||
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
|
||||
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
|
||||
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
|
||||
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
|
||||
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
|
||||
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
// 134 words
|
||||
const unsigned int kFragmentModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u,
|
||||
0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u,
|
||||
0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u,
|
||||
0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
|
||||
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
|
||||
0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au,
|
||||
0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u,
|
||||
0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u,
|
||||
0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
|
||||
0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u,
|
||||
0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u,
|
||||
0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
|
||||
// The quad the vertex module transforms. Full-viewport before the scale, so a scale of
|
||||
// 0.5 covers exactly the middle half of each axis and the corners stay background.
|
||||
const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
// The specialization constant ids the two modules declare.
|
||||
constexpr unsigned int kScaleConstantId = 3;
|
||||
constexpr unsigned int kChannelConstantId = 7;
|
||||
|
||||
unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount,
|
||||
unsigned int constantId, unsigned int constantValue, std::string* outLog) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words,
|
||||
static_cast<GLsizei>(wordCount * sizeof(unsigned int)));
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
if (outLog) *outLog = "glShaderBinary rejected the module";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLint isSpirv = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) {
|
||||
if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
glSpecializeShader(shader, "main", 1, &constantId, &constantValue);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled != GL_TRUE) {
|
||||
if (outLog) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
*outLog = std::string(log.data());
|
||||
}
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
|
||||
GLint formatCount = -1;
|
||||
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format";
|
||||
|
||||
std::vector<GLint> formats(static_cast<size_t>(formatCount), 0);
|
||||
glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
|
||||
<< "the count and the list have to describe the same thing";
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
// 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but
|
||||
// GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently.
|
||||
glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule));
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_ENUM));
|
||||
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION))
|
||||
<< "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader";
|
||||
|
||||
// glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with
|
||||
// it - the transition the conformance suite checks explicitly.
|
||||
const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE);
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializeShaderErrorSurfaceMatchesTheExtension) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// 4242 is not one of the module's constant ids. ARB_gl_spirv enumerates that as
|
||||
// GL_INVALID_VALUE, and an erroring GL command has no other effect - so the shader is left
|
||||
// untouched rather than pushed into a failed-compile state.
|
||||
const unsigned int badId = 4242;
|
||||
const unsigned int value = 0;
|
||||
glSpecializeShader(shader, "main", 1, &badId, &value);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Same for an entry point the module does not carry.
|
||||
glSpecializeShader(shader, "notMain", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Neither refusal specialized the shader, so a well-formed call still works.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE);
|
||||
|
||||
// But a SECOND specialization of a shader that HAS been specialized is INVALID_OPERATION
|
||||
// until glShaderBinary re-associates the module.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "re-associating the module makes specialization legal again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 16);
|
||||
ASSERT_GE(height, 16);
|
||||
|
||||
std::string log;
|
||||
// Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies.
|
||||
unsigned int halfBits = 0;
|
||||
const float half = 0.5f;
|
||||
std::memcpy(&halfBits, &half, sizeof(halfBits));
|
||||
|
||||
const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule,
|
||||
sizeof(kVertexModule) / sizeof(kVertexModule[0]),
|
||||
kScaleConstantId, halfBits, &log);
|
||||
ASSERT_NE(vs, 0u) << "vertex: " << log;
|
||||
// Channel 1 is green; the module's own default is 0 (red), so a specialization that did
|
||||
// nothing paints the wrong colour.
|
||||
const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule,
|
||||
sizeof(kFragmentModule) / sizeof(kFragmentModule[0]),
|
||||
kChannelConstantId, 1u, &log);
|
||||
ASSERT_NE(fs, 0u) << "fragment: " << log;
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> programLog(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(programLog.size()), nullptr, programLog.data());
|
||||
FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data();
|
||||
}
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied";
|
||||
EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted";
|
||||
|
||||
// A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the
|
||||
// clear colour - which is what proves constant id 3 reached the vertex module. At the
|
||||
// default scale of 1.0 the quad covers the whole viewport and this pixel would be green.
|
||||
const Rgba8 corner = painted.At(1, 1);
|
||||
EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied";
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,896 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
|
||||
//
|
||||
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
|
||||
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
|
||||
// case here is the reduced form of a conformance body that fails on a device:
|
||||
//
|
||||
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
|
||||
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
|
||||
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
|
||||
// two backends reach it by completely different routes - DirectGLES has to name a
|
||||
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
|
||||
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
|
||||
//
|
||||
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
|
||||
// user-declared per-vertex interface block, both read back out of gl_in[] by the
|
||||
// evaluation stage and only then captured. This is the shape of
|
||||
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
|
||||
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
|
||||
// the family still fails: gl_Position arrives, and everything travelling beside it in
|
||||
// the same patch does not.
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
|
||||
// absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure instead of looking like an ordinary numeric mismatch.
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class TessellationXfbCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static GLint MaxPatchVertices() {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
|
||||
DrainErrors();
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over a single patch. Returns the capture buffer read back as
|
||||
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
|
||||
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
|
||||
float expected, float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture never reached "
|
||||
<< "these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// Defined below the shader builders it uses. `withPointSize` is the conformance
|
||||
// body's own should_pass_pointsize_data axis.
|
||||
void RunPerVertexPayloadCase(bool withPointSize);
|
||||
|
||||
// Why the gl_PointSize cases cannot be run here, or empty when they can.
|
||||
//
|
||||
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
|
||||
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
|
||||
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
|
||||
// has no query that reports either, so this probes for it by running a program.
|
||||
//
|
||||
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
|
||||
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
|
||||
// compares that against the identical program without the write. A backend that
|
||||
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
|
||||
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
|
||||
// comes back untouched too - which is a capability answer, not a capture answer. If
|
||||
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
|
||||
// the cases run and FAIL rather than skipping on an unrelated breakage.
|
||||
//
|
||||
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
|
||||
// a `return`, so a void helper would leave only the helper and let the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyPointSizeCasesCannotRun();
|
||||
|
||||
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
|
||||
// extensions are independent (Loader models them as two PointSizeTier fields fed by
|
||||
// four distinct strings, and neither implies the other), so a driver with
|
||||
// tessellation point size and no geometry point size passes the probe above and
|
||||
// still cannot run the case below. Same two-program shape, one stage over.
|
||||
//
|
||||
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
|
||||
// a hardcoded frontend constant (256) with no capability behind it, so "does this
|
||||
// stack have a geometry stage at all" can only be answered by trying to build one -
|
||||
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
|
||||
// reason.
|
||||
std::string WhyGeometryPointSizeCaseCannotRun();
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Built-ins captured BY NAME from the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
const char* const kMinimalVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kMinimalTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Values no stale buffer would hold by accident. The two sources differ ONLY by
|
||||
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
|
||||
// built-in is not even declared in a tessellation stage without
|
||||
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
|
||||
const char* const kPositionTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
gl_PointSize = 5.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The two probe programs. They differ by one statement; both capture `probe_value`,
|
||||
// which has nothing to do with point size.
|
||||
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 3.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const auto probeCaptures = [&](const char* tessEvalSource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"probe_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
|
||||
DrainErrors();
|
||||
return captured[0] == 42.0f;
|
||||
};
|
||||
|
||||
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
|
||||
if (withPointSize) return {};
|
||||
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
|
||||
// The control failed too, so nothing here is about point size.
|
||||
return {};
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
|
||||
"program captures an ordinary varying with the gl_PointSize write removed and captures "
|
||||
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
|
||||
"Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
// point_mode with every level at 1 emits three points, all carrying the same
|
||||
// constant; only the first record has to be right for the mechanism to be proven.
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The per-vertex payload the control stage hands to the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body's own shapes, reduced to one patch and parameterised by the
|
||||
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
|
||||
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
|
||||
// which it varies together with point_mode - and which decides whether the whole
|
||||
// program even involves the per-vertex built-in that ESSL gates behind an extension.
|
||||
std::string PayloadVertexSource(bool withPointSize) {
|
||||
return R"(#version 420 core
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
|
||||
R"(};
|
||||
void main()
|
||||
{
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(vertices = )" + std::to_string(outputVertices) +
|
||||
R"() out;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_out[];
|
||||
out OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} result[];
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize
|
||||
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
|
||||
: "") +
|
||||
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
|
||||
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
|
||||
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
|
||||
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
|
||||
gl_InvocationID + 3, gl_InvocationID + 4);
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
|
||||
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
|
||||
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
|
||||
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(isolines, equal_spacing, ccw, point_mode) in;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(};
|
||||
in OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} tc_data[];
|
||||
|
||||
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
|
||||
R"(out vec4 te_position;
|
||||
out vec2 te_value1;
|
||||
out flat ivec4 te_value2;
|
||||
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
|
||||
R"( te_position = vec4 (0.0);
|
||||
te_value1 = vec2 (0.0);
|
||||
te_value2 = ivec4(0);
|
||||
|
||||
for (int n = 0; n < )" + std::to_string(inputVertices) +
|
||||
R"(; ++n)
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
|
||||
R"( te_position += gl_in [n].gl_Position;
|
||||
te_value1 += tc_data[n].value1;
|
||||
te_value2 += tc_data[n].value2;
|
||||
}
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// The reduced conformance body. `withPointSize` selects between its two halves;
|
||||
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
|
||||
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
|
||||
// evaluation stage - is the same on both.
|
||||
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (withPointSize) {
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
}
|
||||
const GLint patchVertices = MaxPatchVertices();
|
||||
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
|
||||
|
||||
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
|
||||
// the control stage runs that many invocations and every one of them contributes.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
|
||||
if (withPointSize) varyings.push_back("te_pointsize");
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
|
||||
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
|
||||
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
varyings);
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
float referencePointSize = 0.0f;
|
||||
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
float referenceValue1[2] = {0.0f, 0.0f};
|
||||
int referenceValue2[4] = {0, 0, 0, 0};
|
||||
for (int n = 0; n < patchVertices; ++n) {
|
||||
referencePointSize += 1.0f / static_cast<float>(n + 1);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
referencePosition[c] += static_cast<float>(n * 4 + c);
|
||||
referenceValue2[c] += n + 1 + c;
|
||||
}
|
||||
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
|
||||
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
|
||||
}
|
||||
|
||||
// isolines with every level at 1 emits two points; the record stride is
|
||||
// vec4 + vec2 + ivec4 [+ float] components.
|
||||
const std::size_t stride = withPointSize ? 11 : 10;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
|
||||
<< "te_position." << c << " (gl_in[].gl_Position)";
|
||||
}
|
||||
for (int c = 0; c < 2; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
|
||||
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
|
||||
}
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const std::size_t index = static_cast<std::size_t>(6 + c);
|
||||
ASSERT_LT(index, captured.size());
|
||||
int actual = 0;
|
||||
std::memcpy(&actual, &captured[index], sizeof(actual));
|
||||
EXPECT_EQ(actual, referenceValue2[c])
|
||||
<< "te_value2." << c << " (the user per-vertex block's integer member)";
|
||||
}
|
||||
if (withPointSize) {
|
||||
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
|
||||
<< "te_pointsize (gl_in[].gl_PointSize)";
|
||||
}
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
|
||||
RunPerVertexPayloadCase(false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same built-in, one stage over.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
|
||||
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
|
||||
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
|
||||
// extensions that do not imply each other, so the geometry arm is a second code path
|
||||
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
|
||||
// shader, so without this case the arm ships untested.
|
||||
const char* const kPointSizeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = 4.0;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
// The control: identical but for the gl_PointSize write, so the pair answers "can this
|
||||
// stack host a geometry stage that names the built-in" without asking anything about
|
||||
// capture.
|
||||
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
|
||||
const auto probeCaptures = [&](const char* geometrySource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> poison(1, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
|
||||
GL_STATIC_DRAW);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
float captured = kPoison;
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
|
||||
&captured);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
DrainErrors();
|
||||
return captured == 7.0f;
|
||||
};
|
||||
|
||||
if (probeCaptures(kPointSizeGeometrySource)) return {};
|
||||
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
|
||||
// The control failed too, so this stack cannot run a capturing geometry stage at
|
||||
// all - which is not what this case is about, and is the question the dead
|
||||
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
|
||||
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
|
||||
// and goes red on its own if that is what actually broke.
|
||||
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
|
||||
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
|
||||
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
|
||||
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
|
||||
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
const std::vector<float> poison(2, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(2, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
|
||||
"the stage carrying it fails to compile";
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The conformance body's own READBACK, which is not glGetBufferSubData.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Every case above reads the capture back with glGetBufferSubData because that is the
|
||||
// shortest path to the bytes. The conformance bodies do something else: they respecify
|
||||
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
|
||||
// while it is simultaneously bound to indexed capture point 0, and then read it with
|
||||
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
|
||||
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
|
||||
// call, so the sequence itself is worth pinning: none of the map path's error conditions
|
||||
// may fire, and the mapped bytes must be the captured ones.
|
||||
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
|
||||
for (int iteration = 0; iteration < 2; ++iteration) {
|
||||
// Respecified through the generic binding, exactly as the conformance body does,
|
||||
// while the same buffer is still bound to capture point 0.
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
|
||||
|
||||
const auto* mapped =
|
||||
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
|
||||
GL_MAP_READ_BIT));
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
|
||||
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
|
||||
const std::vector<float> captured(mapped, mapped + kFloats);
|
||||
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
|
||||
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
|
||||
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
|
||||
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
|
||||
// without asking for it does not merely lose the value - the stage fails to compile
|
||||
// and the whole program is replaced by program 0.
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
|
||||
RunPerVertexPayloadCase(true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same capture through a PROGRAM PIPELINE OBJECT.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body runs each of its configurations twice: once with a monolithic
|
||||
// program object and once with a pipeline of four separable programs, the capture
|
||||
// declared on the separable EVALUATION program. That second shape goes through the
|
||||
// hidden composite the pipeline object builds for the draw, and it is the only place a
|
||||
// tessellation capture and the composite meet - so the capture list has to survive being
|
||||
// taken from a program that is not the one bound.
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
// One separable program per stage. Only the evaluation program carries the capture
|
||||
// list, because it is the one whose outputs are captured.
|
||||
const auto buildSeparable = [&](GLenum stage, const char* source,
|
||||
const std::vector<const char*>& varyings) -> GLuint {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
glAttachShader(program, shader);
|
||||
if (!varyings.empty()) {
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(shader);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
};
|
||||
|
||||
m_buildLog.clear();
|
||||
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
|
||||
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
|
||||
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
|
||||
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
|
||||
const GLuint evalProgram =
|
||||
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
|
||||
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
|
||||
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
|
||||
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
|
||||
|
||||
GLuint pipeline = 0;
|
||||
glGenProgramPipelines(1, &pipeline);
|
||||
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
|
||||
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(0);
|
||||
glBindProgramPipeline(pipeline);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(kFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glBindProgramPipeline(0);
|
||||
glDeleteProgramPipelines(1, &pipeline);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,908 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
|
||||
//
|
||||
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
|
||||
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
|
||||
// application's capability check passes, it takes the texture-view path, and the view texture it
|
||||
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
|
||||
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
|
||||
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
|
||||
// that aliased nothing.
|
||||
//
|
||||
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
|
||||
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
|
||||
// * writes through either name are visible through the other (CoherencyIsBidirectional), and
|
||||
// * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
|
||||
// entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
|
||||
// parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
|
||||
// GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
|
||||
//
|
||||
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
|
||||
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
|
||||
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
|
||||
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
|
||||
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
|
||||
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
|
||||
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
|
||||
// see NoExtensionSupportIsRefusedRatherThanFaked.
|
||||
//
|
||||
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
|
||||
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
|
||||
// through the two names, or a value that changed after a write through the other name.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kSize = 64;
|
||||
// The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
|
||||
// uniform image would otherwise pass a test that only ever looked at covered texels.
|
||||
constexpr int kUncoveredTop = 16;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform vec4 uRect; // x0, y0, x1, y1 in NDC
|
||||
uniform float uDepth; // NDC z
|
||||
void main() {
|
||||
vec2 p = mix(uRect.xy, uRect.zw, aPos);
|
||||
gl_Position = vec4(p, uDepth, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
|
||||
// draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
|
||||
// COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
|
||||
// error or clobber attachment 0.
|
||||
constexpr const char* kCoverageFragmentSource = R"(#version 330 core
|
||||
layout (location = 0) out vec4 outColor;
|
||||
layout (location = 1) out float outUnused;
|
||||
void main() {
|
||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
outUnused = 1.0 / 255.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
|
||||
// D24S8 image through two GL texture names bound to two units in this one invocation.
|
||||
// `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
|
||||
// at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
|
||||
// it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
|
||||
constexpr const char* kShadingFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uCoverage; // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
|
||||
uniform sampler2D uDepthView; // the VIEW, DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
|
||||
float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
|
||||
gl_FragDepth = depth;
|
||||
}
|
||||
)";
|
||||
|
||||
// Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
|
||||
// and unpacks the word back into the four bytes it was written as.
|
||||
constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
|
||||
uniform usampler2D uWords;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
|
||||
outColor = vec4(float((word ) & 0xFFu) / 255.0,
|
||||
float((word >> 8) & 0xFFu) / 255.0,
|
||||
float((word >> 16) & 0xFFu) / 255.0,
|
||||
float((word >> 24) & 0xFFu) / 255.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 330 core
|
||||
uniform sampler2D uTexture;
|
||||
uniform float uLod;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
|
||||
std::to_string(c.a) + ")";
|
||||
}
|
||||
|
||||
class TextureViewScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!TextureViewUsable()) {
|
||||
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
|
||||
<< " (GL_ARB_texture_view not advertised)";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint rbo : m_rbos) {
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
}
|
||||
m_rbos.clear();
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
if (m_vao != 0) {
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
if (m_vbo != 0) {
|
||||
glDeleteBuffers(1, &m_vbo);
|
||||
m_vbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial same-format full-range view. It exercises nothing the cases below test,
|
||||
// so a backend that simply does not have the feature skips instead of failing every
|
||||
// one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
|
||||
bool TextureViewUsable() {
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint view = 0;
|
||||
glGenTextures(1, &view);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(1, &view);
|
||||
glDeleteTextures(1, &storage);
|
||||
return usable;
|
||||
}
|
||||
|
||||
GLuint MakeVao() {
|
||||
if (m_vao != 0) return m_vao;
|
||||
// A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
|
||||
// one buffer serves every draw here.
|
||||
static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
return m_vao;
|
||||
}
|
||||
|
||||
GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
|
||||
EXPECT_NE(program, 0u) << "program failed to build: " << error;
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint MakeFbo() {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
|
||||
// here views. Levels beyond 1 stay undefined until a caller fills them.
|
||||
GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
|
||||
const GLuint texture = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
|
||||
glUseProgram(program);
|
||||
glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
|
||||
const GLint depthLocation = glGetUniformLocation(program, "uDepth");
|
||||
if (depthLocation >= 0) glUniform1f(depthLocation, depth);
|
||||
glBindVertexArray(MakeVao());
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
// Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
|
||||
Image ReadFbo(GLuint fbo, int width, int height) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
return ReadPixels(width, height);
|
||||
}
|
||||
|
||||
// Every pixel of the inclusive region must match `expected` within `tolerance` per
|
||||
// channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
|
||||
// three of four vertices carrying stale data still paints a correct centre pixel.
|
||||
void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
|
||||
const char* what) {
|
||||
int offenders = 0;
|
||||
Rgba8 firstOffender{};
|
||||
int firstX = -1;
|
||||
int firstY = -1;
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const Rgba8 actual = image.At(x, y);
|
||||
const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
|
||||
std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
|
||||
std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
|
||||
std::abs(int(actual.a) - int(expected.a)) <= tolerance;
|
||||
if (!ok) {
|
||||
if (offenders == 0) {
|
||||
firstOffender = actual;
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
}
|
||||
++offenders;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
|
||||
<< (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
|
||||
<< ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
|
||||
<< Describe(expected) << " +/- " << tolerance;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
std::vector<GLuint> m_rbos;
|
||||
std::vector<GLuint> m_programs;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// The driving case: the Better Clouds full-mode pipeline, in its real order.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// --- Resources.java:230-251, in order ---------------------------------------------
|
||||
const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint coverage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
|
||||
const GLuint coverageFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
|
||||
const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
|
||||
glDrawBuffers(1, drawBuffers);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "the coverage framebuffer is incomplete; the mod would silently demote to its "
|
||||
"fallback configuration here (Resources.java:187-209)";
|
||||
|
||||
// The view is made from a name glGenTextures has only RESERVED - it has never been
|
||||
// bound, so glTextureView has to instantiate the texture object itself.
|
||||
const GLuint coverageDepthView = MakeTexture();
|
||||
glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";
|
||||
|
||||
// The two names must be distinguishable through the queries, or nothing below proves
|
||||
// which one produced a sample.
|
||||
GLint parentMode = 0;
|
||||
GLint viewMode = 0;
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
|
||||
EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
|
||||
<< "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
|
||||
"the parent is precisely what a texture view exists to avoid";
|
||||
|
||||
// --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
|
||||
const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
|
||||
ASSERT_NE(coverageProgram, 0u);
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
|
||||
glClearDepth(0.0);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilMask(0xff);
|
||||
glClearStencil(0);
|
||||
// The coverage COUNT: one increment per depth-passing cloud fragment.
|
||||
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
|
||||
glStencilFunc(GL_ALWAYS, 0xff, 0xff);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Quad 1 covers everything above the uncovered strip, at window depth 0.25.
|
||||
const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
|
||||
DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
|
||||
// Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
|
||||
// so it both passes the depth test and increments the stencil a second time.
|
||||
DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";
|
||||
|
||||
// --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
|
||||
// A different draw framebuffer, exactly as the mod does (it hands the frame back to
|
||||
// Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
|
||||
// being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
GLuint destinationDepth = 0;
|
||||
glGenRenderbuffers(1, &destinationDepth);
|
||||
m_rbos.push_back(destinationDepth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
// The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
|
||||
ASSERT_NE(shadingProgram, 0u);
|
||||
glUseProgram(shadingProgram);
|
||||
// Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
|
||||
// own unit assignment (Resources.java:309/311).
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, coverage);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
|
||||
glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
||||
DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";
|
||||
|
||||
const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
|
||||
// A stub view samples (0,0,0,1), which fails the green channel of both covered
|
||||
// regions; a view that inherited the parent's stencil aspect fails them too.
|
||||
constexpr int kTolerance = 3;
|
||||
ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
|
||||
"the uncovered strip must read coverage 0 and cleared depth 0");
|
||||
ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
|
||||
"one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
|
||||
ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
|
||||
kTolerance,
|
||||
"two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
|
||||
"through the view");
|
||||
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Storage sharing, in both directions. This is the assertion a copy-based emulation
|
||||
// fails, and the reason the no-EXT path refuses rather than emulates.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Render red through the PARENT's name...
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// ...and read it back through the VIEW's.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "a texture view must be attachable like any other texture";
|
||||
Image throughView = ReadFbo(viewFbo, kSize, kSize);
|
||||
ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
|
||||
"a write through the parent must be visible through the view");
|
||||
|
||||
// Now the other direction: write green through the VIEW, read through the PARENT.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const Image throughParent = ReadFbo(fbo, kSize, kSize);
|
||||
ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
|
||||
"a write through the view must be visible through the parent - they are one "
|
||||
"storage, not two");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Format reinterpretation within a view class (GL 4.6 core table 8.21).
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
|
||||
// other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
|
||||
// answer a fact about the BITS rather than about the colour.
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = 0x40;
|
||||
texels[i + 1] = 0x80;
|
||||
texels[i + 2] = 0xC0;
|
||||
texels[i + 3] = 0xFF;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";
|
||||
|
||||
// NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
|
||||
// so a test that only asserted the view's answer could not tell "the reinterpret is
|
||||
// wrong" from "the seed never reached the GPU at all". Read the same texels through
|
||||
// the parent's own format first.
|
||||
const GLuint parentFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image seeded = ReadFbo(parentFbo, kSize, kSize);
|
||||
ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
|
||||
"control: the storage must hold the seeded byte pattern before any view reads it");
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
GLint viewFormat = 0;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";
|
||||
|
||||
// A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
|
||||
// assertion below is that the view and this texture sample IDENTICALLY.
|
||||
//
|
||||
// Comparing against a reference texture rather than against a hard-coded colour is
|
||||
// deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
|
||||
// about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
|
||||
// with a raw-EGL program that reproduces the same wrong decode with NO view in play).
|
||||
// Holding both sides to the same driver factors that out completely: whatever the
|
||||
// driver makes of a usampler2D fetch, the view has to make the same thing of it, or
|
||||
// it is not delivering the storage's bits. A view that samples zero, that lands on
|
||||
// the wrong texels, or that lost its format still fails.
|
||||
constexpr std::uint32_t kExpectedWord = 0xFFC08040u; // little-endian A,B,G,R
|
||||
const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
|
||||
std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
|
||||
glBindTexture(GL_TEXTURE_2D, reference);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
const auto decodeThrough = [&](GLuint texture) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(glGetUniformLocation(program, "uWords"), 0);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
|
||||
return ReadFbo(destination.fbo, kSize, kSize);
|
||||
};
|
||||
|
||||
const Image throughReference = decodeThrough(reference);
|
||||
const Image throughView = decodeThrough(view);
|
||||
|
||||
// Guard against the degenerate agreement of two black images: the reference must
|
||||
// itself carry something, or "identical" would prove nothing.
|
||||
const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
|
||||
ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
|
||||
<< "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";
|
||||
|
||||
std::size_t mismatches = 0;
|
||||
for (int y = 0; y < kSize; ++y) {
|
||||
for (int x = 0; x < kSize; ++x) {
|
||||
if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(mismatches, 0u)
|
||||
<< "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
|
||||
"holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
|
||||
<< ", reference centre is " << Describe(referenceTexel);
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Sub-ranges: one mip level of two, and one layer of an array.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
|
||||
// Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
|
||||
glViewport(0, 0, kSize / 2, kSize / 2);
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
GLint immutableLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 1);
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
// GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
|
||||
EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";
|
||||
|
||||
// The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
|
||||
// the blue half-size image.
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
|
||||
ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
|
||||
"the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
|
||||
}
|
||||
|
||||
TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kChosenLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// A different colour per layer, so a view that lost its layer offset reads the wrong
|
||||
// one rather than merely reading nothing.
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";
|
||||
|
||||
GLint minLayer = -1;
|
||||
GLint numLayers = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLayer, kChosenLayer);
|
||||
EXPECT_EQ(numLayers, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image sliced = ReadFbo(viewFbo, kSize, kSize);
|
||||
const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
|
||||
static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
|
||||
ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
|
||||
"a single-layer 2D view of an array must address the layer it named");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Writing THROUGH a layer-sliced view. The read direction is covered above; this is the
|
||||
// write direction, and it is the one that can corrupt the parent rather than merely
|
||||
// return the wrong pixels - a view whose texel path forgot its layer origin writes over
|
||||
// the parent's layer 0 while the application believes it addressed layer minLayer.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, WritingThroughALayerSlicedViewLandsOnItsOwnLayers) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kViewMinLayer = 2;
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const auto layerFill = [](int layer) {
|
||||
return Rgba8{static_cast<std::uint8_t>(10 + layer * 20),
|
||||
static_cast<std::uint8_t>(200 - layer * 20), 30, 255};
|
||||
};
|
||||
// Seeded by CPU sub-image rather than by rendering, deliberately: this scenario is
|
||||
// about the view's LAYER ORIGIN, and seeding through the GPU would additionally
|
||||
// depend on a CPU sub-image reaching a layer whose content the GPU wrote - which
|
||||
// DirectVulkan does not currently do even for a plain array texture (no view
|
||||
// involved), and which would make a failure here unattributable.
|
||||
const auto uploadLayer = [&](GLuint texture, int layer, Rgba8 colour) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = colour.r;
|
||||
texels[i + 1] = colour.g;
|
||||
texels[i + 2] = colour.b;
|
||||
texels[i + 3] = colour.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
};
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
uploadLayer(storage, layer, layerFill(layer));
|
||||
}
|
||||
const GLuint fbo = MakeFbo();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the layers raised an error";
|
||||
|
||||
// A two-layer window starting at layer 2, so a lost offset lands on layer 0 - which
|
||||
// the assertions below would see as an untouched layer that moved.
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, kViewMinLayer, 2);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Write the view's OWN layer 0, i.e. the storage's layer 2.
|
||||
constexpr Rgba8 kPainted{255, 0, 255, 255};
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i + 0] = kPainted.r;
|
||||
texels[i + 1] = kPainted.g;
|
||||
texels[i + 2] = kPainted.b;
|
||||
texels[i + 3] = kPainted.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, view);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
texels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "writing through the view raised an error";
|
||||
|
||||
// POSITIVE CONTROL, through the parent's own name and into a layer outside the view's
|
||||
// window. It makes the assertions below able to tell "the view lost its layer origin"
|
||||
// from "a CPU sub-image into this array does not reach the GPU at all", which is a
|
||||
// different question and not one a texture view can answer.
|
||||
constexpr Rgba8 kControl{0, 0, 255, 255};
|
||||
std::vector<std::uint8_t> controlTexels(texels.size());
|
||||
for (std::size_t i = 0; i < controlTexels.size(); i += 4) {
|
||||
controlTexels[i + 0] = kControl.r;
|
||||
controlTexels[i + 1] = kControl.g;
|
||||
controlTexels[i + 2] = kControl.b;
|
||||
controlTexels[i + 3] = kControl.a;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
controlTexels.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the control write raised an error";
|
||||
|
||||
// Read every layer of the PARENT back: only the one the view's layer 0 maps to may
|
||||
// have changed.
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
const Image image = ReadPixels(kSize, kSize);
|
||||
Rgba8 expected = layerFill(layer);
|
||||
const char* what = "a layer outside the view's window must not have been written";
|
||||
if (layer == kViewMinLayer) {
|
||||
expected = kPainted;
|
||||
what = "the view's layer 0 must be the storage layer it named";
|
||||
} else if (layer == 1) {
|
||||
expected = kControl;
|
||||
what = "control: a sub-image written through the PARENT must reach its layer";
|
||||
}
|
||||
ExpectRegion(image, 0, kSize - 1, 0, kSize - 1, expected, 2, what);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Views of views compose; the composed view still reaches the ROOT storage.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLevels = 3;
|
||||
const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
|
||||
const GLuint seedFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
for (int level = 0; level < kLevels; ++level) {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
|
||||
glViewport(0, 0, kSize >> level, kSize >> level);
|
||||
glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
|
||||
// root's level 2. GL 4.6 core 8.18 makes the offsets add.
|
||||
const GLuint firstView = MakeTexture();
|
||||
glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
const GLuint secondView = MakeTexture();
|
||||
glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";
|
||||
|
||||
GLint minLevel = -1;
|
||||
GLint numLevels = -1;
|
||||
glBindTexture(GL_TEXTURE_2D, secondView);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
|
||||
EXPECT_EQ(numLevels, 1);
|
||||
|
||||
const GLuint viewFbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
|
||||
ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
|
||||
"the composed view must land on the root's level 2");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// GL name-deletion semantics: the storage outlives the original's NAME.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const GLuint fbo = MakeFbo();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The NAME goes; the storage may not, because a view still references it
|
||||
// (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
|
||||
glDeleteTextures(1, &storage);
|
||||
EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
// Sample the view through a shader, so the answer comes from a live descriptor rather
|
||||
// than from an attachment the frontend might have kept alive by other means.
|
||||
ColorFbo destination = MakeColorFbo(kSize, kSize);
|
||||
ASSERT_NE(destination.fbo, 0u);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
|
||||
ASSERT_NE(program, 0u);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
|
||||
glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
|
||||
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
|
||||
ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
|
||||
"the view must still reach its storage after the original's name was deleted");
|
||||
DestroyColorFbo(destination);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,506 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A PROGRAM DECLARES AN IMAGE-BACKED RESOURCE AND THE APPLICATION BINDS NOTHING.
|
||||
//
|
||||
// The sibling of GuiBatchScenario's MeshesBlockLeftUnbound, one descriptor kind further out.
|
||||
// That one pinned an unbound shader storage BLOCK; the same "nothing is bound, so lose the
|
||||
// whole draw" shape survived in the three image-backed kinds:
|
||||
//
|
||||
// * `samplerBuffer` - a texture unit with no buffer texture on it, and a buffer texture with
|
||||
// no GL buffer attached to it. Both make the sampler INCOMPLETE (GL 4.6
|
||||
// core 8.9, 8.24), and sampling an incomplete texture returns undefined
|
||||
// VALUES. It is not an error and it is not a lost draw.
|
||||
// * `imageBuffer` - an image unit with nothing on it. GL 4.6 core 8.26 is explicit: loads
|
||||
// return zero and stores are discarded.
|
||||
// * `image2D` - the same rule, through a VkImageView rather than a VkBufferView.
|
||||
//
|
||||
// Vulkan has no such thing as an unwritten descriptor, so DirectVulkan's descriptor resolution
|
||||
// used to answer "no valid descriptor" and both SetupDraw and DispatchCompute skip everything on
|
||||
// that answer - the draw or dispatch simply never happened, silently. Every test below asserts
|
||||
// on the OTHER work in the same shader: the pixels the fragment stage painted, or the buffer the
|
||||
// dispatch filled. All of it is unrelated to the unbound resource and all of it disappeared.
|
||||
//
|
||||
// The unbound resource is STATICALLY USED in every case, because an unreferenced one is
|
||||
// optimised out before it ever reaches a descriptor and would prove nothing. Where the use is a
|
||||
// read it sits behind a uniform-controlled branch that is false at runtime - the descriptor is
|
||||
// declared and must be written, but no undefined value reaches an assertion. Where it is a write
|
||||
// (the `writeonly` cases, which is how the real workloads spell it) it is unconditional: GL says
|
||||
// the store is discarded, so there is nothing to guard against.
|
||||
//
|
||||
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound unit to the GLES driver,
|
||||
// which does what GL says, so it is the control - every test here must stay green on both.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
constexpr int kElements = 4;
|
||||
|
||||
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
|
||||
// vertex fetch can be confused with the descriptor question under test.
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The assertion in every draw case: opaque green everywhere. The unbound resource
|
||||
// contributes nothing to it - u_readUnbound is 0, so the fetch never runs - but the
|
||||
// descriptor for it still has to exist, which is the point.
|
||||
constexpr const char* kSamplerBufferFragmentSource = R"(#version 430 core
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kSamplerBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
uniform samplerBuffer u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
uint value = index + 1u;
|
||||
if (u_readUnbound != 0) {
|
||||
value += uint(texelFetch(u_unbound, 0).r);
|
||||
}
|
||||
g_data[index] = value;
|
||||
}
|
||||
)";
|
||||
|
||||
// writeonly, and the store is unconditional: this is how AcceleratedRendering and the
|
||||
// conformance cases spell an image the shader only produces into. GL discards the store
|
||||
// when the unit is empty; nothing here reads it back.
|
||||
constexpr const char* kImageBufferFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, 0, uvec4(7u));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImageBufferComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, int(index), uvec4(7u));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
|
||||
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
|
||||
// level and no mip chain - GL calls the second one incomplete for the initial
|
||||
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
|
||||
// texture the backend then fails to back.
|
||||
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texture(u_unbound, vec2(0.0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
|
||||
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
|
||||
// unused one at an empty unit, so whichever of the two is unused has to have a
|
||||
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
|
||||
// cannot stand in for it.
|
||||
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform sampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, ivec2(0), 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
||||
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
||||
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
||||
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
||||
// been created.
|
||||
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform usampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform isampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
imageStore(u_unbound, ivec2(0, 0), vec4(1.0));
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// No layout format at all, which GLSL 4.20 allows for a write-only image. The reflection
|
||||
// then carries NO format for the binding, so the placeholder descriptor can only be
|
||||
// constrained by the declaration's numeric class - a different route through the fix than
|
||||
// every typed case above.
|
||||
constexpr const char* kFormatlessImage2DComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
layout(binding = 0) uniform writeonly image2D u_unbound;
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.x;
|
||||
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
class UnboundImageDescriptorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenBuffers(1, &m_storage);
|
||||
// The harness shares one context across every scenario in the process, so an
|
||||
// earlier one may well have left a texture on unit 0 or an image on unit 0. The
|
||||
// whole subject here is that nothing is bound, so say so rather than assume it.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_storage != 0) glDeleteBuffers(1, &m_storage);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
// Each case needs exactly one kind of opaque uniform in one stage, and a host with
|
||||
// none of that kind there would report a failure that is about the host, not the fix.
|
||||
// Asked for by the limit that governs the kind under test and no other: a guard that
|
||||
// over-asks turns into a silent skip of the very thing the case exists for.
|
||||
static bool LimitIsAtLeastOne(GLenum limit) {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(limit, &value);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return value >= 1;
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Fills a four-element SSBO with 1..4 while the unbound resource is declared and
|
||||
// statically used. Zeros everywhere mean the dispatch never ran.
|
||||
void ExpectDispatchStillRuns(const char* source, const char* what) {
|
||||
m_program = MakeComputeProgram(source);
|
||||
ASSERT_NE(m_program, 0u);
|
||||
|
||||
const std::vector<unsigned int> zeros(static_cast<std::size_t>(kElements), 0u);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
||||
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_storage);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting up the output buffer raised a GL error";
|
||||
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDispatchCompute(kElements, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error (" << what << ")";
|
||||
|
||||
std::vector<unsigned int> values(static_cast<std::size_t>(kElements), 0xDEADBEEFu);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
||||
<< "element " << i << " came back as " << values[static_cast<std::size_t>(i)]
|
||||
<< "; zero everywhere means the whole dispatch was dropped over the unbound " << what;
|
||||
}
|
||||
}
|
||||
|
||||
// Paints the whole render target green while the unbound resource is declared and
|
||||
// statically used. A black target means the draw never happened.
|
||||
void ExpectDrawStillRuns(const char* fragmentSource, const char* what) {
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, fragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
||||
if (readUnbound != -1) {
|
||||
glUniform1i(readUnbound, 0);
|
||||
}
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error (" << what << ")";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << "the readback came back empty";
|
||||
// Whole-region, not a centre pixel: the quad covers the target exactly, so
|
||||
// anything short of all of it is a failure worth naming.
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0,
|
||||
std::string("the quad drawn with an unbound ") + what))
|
||||
<< "an all-black target means the draw was dropped over the unbound " << what;
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_storage = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- uniform samplerBuffer (VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS)) {
|
||||
GTEST_SKIP() << "the compute stage has no texture image units";
|
||||
}
|
||||
ExpectDispatchStillRuns(kSamplerBufferComputeSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "samplerBuffer");
|
||||
}
|
||||
|
||||
// The other way a texel-buffer descriptor comes out empty: the unit HAS a buffer texture, but
|
||||
// no glTexBuffer ever attached a buffer object to it. GL calls that texture incomplete, which
|
||||
// is undefined data and not a lost draw - a separate site in the resolve from the one above,
|
||||
// and it used to return false too.
|
||||
TEST_F(UnboundImageDescriptorScenario, ABufferTextureWithNoAttachedBufferDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
// Deliberately no glTexBuffer: the texture exists and is bound, and has no store.
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding an empty buffer texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "buffer texture with no attached buffer");
|
||||
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// ---- writeonly imageBuffer (VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) --------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImageBufferComputeSource, "imageBuffer");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImageBufferFragmentSource, "imageBuffer");
|
||||
}
|
||||
|
||||
// ---- writeonly image2D (VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) -------------------------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kImage2DComputeSource, "image2D");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
||||
}
|
||||
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
||||
}
|
||||
|
||||
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
|
||||
}
|
||||
|
||||
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
|
||||
// unit whose DEFAULT texture object has an image but no mip chain.
|
||||
//
|
||||
// DirectVulkan resolved such a binding twice, through two different predicates that
|
||||
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
|
||||
// pre-syncs and transitions every texture the draw will sample, asked only whether the
|
||||
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
|
||||
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
|
||||
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
|
||||
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
|
||||
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
|
||||
// a SIGSEGV inside the draw, not a degraded picture.
|
||||
//
|
||||
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
|
||||
// texture a base level, so the FIRST case in a process survived and every later one with an
|
||||
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
|
||||
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
|
||||
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
|
||||
// object bound there a FORMAT and a zero-sized level - which is what a bare
|
||||
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
|
||||
// state reset issues for every texture target. That combination is the whole point:
|
||||
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
|
||||
// and the texture stays in the sampled set;
|
||||
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
|
||||
// true and the descriptor holds the fallback instead;
|
||||
// * and it has no valid mip level, so the sync declines and hands back nullptr.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
GTEST_SKIP() << "the compute stage has no image uniforms";
|
||||
}
|
||||
ExpectDispatchStillRuns(kFormatlessImage2DComputeSource, "format-less image2D");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user