mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
200
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 |
@@ -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
+139
@@ -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
|
||||
@@ -414,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
|
||||
@@ -464,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}
|
||||
@@ -479,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}
|
||||
)
|
||||
@@ -695,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()
|
||||
|
||||
+164
-25
@@ -69,34 +69,34 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EnableGlesTextureView = false;
|
||||
Bool EsprytEnableTextureView = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool EsprytUseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool DisableSubgroup = false;
|
||||
Bool MagmaDisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// FixIterationRPSubgroupScratch below instead). Off by default.
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
@@ -106,12 +106,12 @@ namespace MobileGL::MG_Config {
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||
// verbatim.
|
||||
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||
Bool IterationRPFixBarrier = false;
|
||||
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
@@ -119,7 +119,7 @@ namespace MobileGL::MG_Config {
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
@@ -132,16 +132,39 @@ namespace MobileGL::MG_Config {
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool AvoidExplicitLodBias = false;
|
||||
Bool EsprytAvoidExplicitLodBias = false;
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
|
||||
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
|
||||
// match them by block name and member sequence instead. The Mali ES driver delivers
|
||||
// nothing at all through a located block once a tessellation or geometry stage is in
|
||||
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
|
||||
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
|
||||
// healthy driver - which is what the integration lane does, since llvmpipe and
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -151,15 +174,38 @@ namespace MobileGL::MG_Config {
|
||||
Bool CoherentAsFlush = false;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
Bool TraceSkipAutodestroy = false;
|
||||
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
Bool EsprytDisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
// glTexSubImage from the client pointer instead of staging them through the
|
||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||
// hatch).
|
||||
Bool DisableUnpackRing = false;
|
||||
Bool EsprytDisableUnpackRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
|
||||
// (glBufferSubData / map flushes) back to the immediate driver upload instead
|
||||
// of queueing them for the staged-copy flush through the persistent-mapped
|
||||
// upload ring (negative control / driver-bug escape hatch; the immediate
|
||||
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
|
||||
Bool EsprytDisableUploadRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
|
||||
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
|
||||
// to the upload ring's staged glCopyBufferSubData (negative control / escape
|
||||
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
|
||||
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
@@ -180,10 +226,10 @@ namespace MobileGL::MG_Config {
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
Bool MagmaDisableRobustBufferAccess = false;
|
||||
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
@@ -224,7 +270,7 @@ namespace MobileGL::MG_Config {
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
@@ -236,7 +282,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
-15
@@ -159,37 +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.EnableGlesTextureView = QueryEnvFlag("MOBILEGL_ENABLE_GLES_TEXTURE_VIEW");
|
||||
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||
features.FixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
||||
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
||||
features.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
@@ -197,8 +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
|
||||
@@ -364,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.
|
||||
@@ -389,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
|
||||
@@ -482,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,7 +766,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {4, 3, 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.
|
||||
@@ -995,10 +1012,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
|
||||
// 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,
|
||||
@@ -1180,8 +1198,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||
// above refuses to tell, just with an extra prerequisite met. Set
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
|
||||
extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
@@ -1237,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;
|
||||
@@ -1399,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
|
||||
@@ -1461,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
|
||||
|
||||
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 /
|
||||
@@ -529,7 +637,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// which is what to watch if this ring ever shows up in an RSS regression: it
|
||||
// grows on demand from 4 MiB and is capped, not unbounded.
|
||||
//
|
||||
// False when the feature is disabled (MOBILEGL_DISABLE_UNPACK_RING),
|
||||
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
|
||||
// EXT_buffer_storage / fences are missing, the ES context is not current, or
|
||||
// ring creation already failed under this context. Callers then upload from
|
||||
// the client pointer exactly as before.
|
||||
@@ -544,6 +652,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Largest single staging request the ring can ever satisfy.
|
||||
SizeT UnpackRingMaxBytes();
|
||||
void UnpackRingOnPresent();
|
||||
|
||||
// --- Buffer upload ring ---------------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
|
||||
// (glBufferSubData / non-persistent map flushes) whose destination store may
|
||||
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
|
||||
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
|
||||
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
|
||||
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
|
||||
// each such call serialized against the whole GPU queue (~1 fps while chunks
|
||||
// stream in, and again on every camera pan). App SubData ranges are queued on
|
||||
// the resource instead (the frontend shadow already holds the bytes) and
|
||||
// draw-time sync drains them: bytes staged into this ring, then one
|
||||
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
|
||||
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
|
||||
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
|
||||
// historical immediate-upload path (negative control / escape hatch).
|
||||
void UploadRingOnPresent();
|
||||
} // namespace BufferImpl
|
||||
|
||||
namespace VertexArrayImpl {
|
||||
@@ -961,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
|
||||
@@ -1447,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; }
|
||||
@@ -1517,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,
|
||||
@@ -1547,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,7 +501,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 3, 0},
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
@@ -516,10 +517,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,3,0}, and a list that stopped at OpenGL40 told an
|
||||
// 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,
|
||||
@@ -623,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
|
||||
@@ -739,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;
|
||||
@@ -784,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();
|
||||
}
|
||||
@@ -938,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);
|
||||
@@ -1005,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
|
||||
@@ -1067,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,7 +70,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <spirv-tools/libspirv.h>
|
||||
@@ -74,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
// Where a gl_PerVertex built-in output lives, resolved from the module's annotations.
|
||||
// Named for gl_Position because the clip-space fixup is what it was written for, and it
|
||||
// is still the only shape that pass accepts - but the transform-feedback capture pass
|
||||
// resolves gl_PointSize through the same struct, in which case `vectorTypeId` /
|
||||
// `vectorPtrTypeId` hold the SCALAR float type and its Output pointer rather than a vec4.
|
||||
struct PositionTargetInfo {
|
||||
Uint32 variableId = 0;
|
||||
Uint32 vectorTypeId = 0;
|
||||
@@ -99,6 +107,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_PointSize's counterpart to IsVec4Float32. The two are the only shapes any
|
||||
// gl_PerVertex member this file resolves can have, and each resolver takes whichever
|
||||
// one its built-in is declared with, so a mismatched type declines rather than
|
||||
// producing a mirror the driver would reject.
|
||||
Bool IsFloat32Scalar(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||
auto* floatInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
|
||||
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
|
||||
|
||||
if (outFloatTypeId) *outFloatTypeId = typeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Which of the two shapes above a resolver should accept. A plain function pointer
|
||||
// rather than a std::function: every call site is one of the two free functions.
|
||||
using BuiltInTypeCheckFn = Bool (*)(spvtools::opt::IRContext*, Uint32, Uint32*);
|
||||
|
||||
spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) {
|
||||
if (variable.type_description == nullptr) {
|
||||
return SPVC_BASETYPE_UNKNOWN;
|
||||
@@ -378,9 +403,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return used;
|
||||
}
|
||||
|
||||
void ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) {
|
||||
// What a failed validation says, for a caller that wants to put it in its own message.
|
||||
struct SpirvValidationFailure {
|
||||
String message;
|
||||
Int result = 0;
|
||||
SizeT index = 0;
|
||||
};
|
||||
|
||||
// Returns whether the module validates. The result used to be discarded everywhere: the
|
||||
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
|
||||
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
|
||||
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
|
||||
// the same "not a validating entry point" behaviour PipelineFactory already documents for
|
||||
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
|
||||
//
|
||||
// This function does NOT log the failure at E any more. It used to, unlatched, on the
|
||||
// stated grounds that "reaching here already requires the validation switch to be armed,
|
||||
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
|
||||
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
|
||||
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
|
||||
// The failure text now travels back through `outFailure` so the LATCHED call-site
|
||||
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
|
||||
// is the D-level detail and the process-wide counter the test lanes assert on.
|
||||
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
|
||||
SpirvValidationFailure* outFailure = nullptr) {
|
||||
if (outFailure != nullptr) *outFailure = {};
|
||||
if (spirv.empty()) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
spv_const_binary_t binary = {spirv.data(), spirv.size()};
|
||||
@@ -402,18 +451,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
const char* message =
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
|
||||
const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
|
||||
// The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
|
||||
// asserting on the delta). Bumped for every failed validation, including one a
|
||||
// caller goes on to recover from: a transform that produced an invalid module is
|
||||
// a real defect whether or not this run survived it.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_E(
|
||||
if (outFailure != nullptr) {
|
||||
*outFailure = {String(message), static_cast<Int>(result), index};
|
||||
}
|
||||
MGLOG_D(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
index,
|
||||
message);
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
@@ -429,6 +484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
spvValidatorOptionsDestroy(options);
|
||||
spvContextDestroy(context);
|
||||
return result == SPV_SUCCESS;
|
||||
}
|
||||
|
||||
void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable,
|
||||
@@ -740,8 +796,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveDirectBuiltInTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
|
||||
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
|
||||
if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false;
|
||||
@@ -753,7 +809,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
target.variableId = variableId;
|
||||
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
|
||||
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
if (!typeCheck(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
target.vectorPtrTypeId = varInst->type_id();
|
||||
target.isMember = false;
|
||||
|
||||
@@ -768,15 +824,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return context->get_type_mgr()->GetTypeInstruction(&ptrType);
|
||||
}
|
||||
|
||||
Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveMemberBuiltInTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
|
||||
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
if (memberIndex >= structInst->NumInOperands()) return false;
|
||||
|
||||
const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
|
||||
Uint32 floatTypeId = 0;
|
||||
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false;
|
||||
if (!typeCheck(context, vectorTypeId, &floatTypeId)) return false;
|
||||
|
||||
const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
|
||||
if (vectorPtrTypeId == 0) return false;
|
||||
@@ -804,27 +860,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
// The OUTPUT variable (or gl_PerVertex member) carrying `builtIn`, if the module
|
||||
// declares one of the expected type. Annotations are the search space deliberately:
|
||||
// they survive the link-time sanitize chain's interface delisting, which is the whole
|
||||
// reason EnsureEntryPointInterface exists.
|
||||
Bool FindBuiltInTarget(spvtools::opt::IRContext* context, spv::BuiltIn builtIn,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
Vector<Pair<Uint32, Uint32>> memberCandidates;
|
||||
constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn);
|
||||
constexpr auto kBuiltInPosition = static_cast<Uint32>(spv::BuiltIn::Position);
|
||||
const auto wantedBuiltIn = static_cast<Uint32>(builtIn);
|
||||
|
||||
for (auto& inst : context->module()->annotations()) {
|
||||
if (inst.opcode() == spv::Op::OpDecorate) {
|
||||
if (inst.NumInOperands() < 3) continue;
|
||||
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue;
|
||||
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true;
|
||||
if (inst.GetSingleWordInOperand(2) != wantedBuiltIn) continue;
|
||||
if (ResolveDirectBuiltInTarget(context, inst.GetSingleWordInOperand(0), typeCheck, outTarget)) {
|
||||
return true;
|
||||
}
|
||||
} else if (inst.opcode() == spv::Op::OpMemberDecorate) {
|
||||
if (inst.NumInOperands() < 4) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != wantedBuiltIn) continue;
|
||||
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [structTypeId, memberIndex] : memberCandidates) {
|
||||
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true;
|
||||
if (ResolveMemberBuiltInTarget(context, structTypeId, memberIndex, typeCheck, outTarget)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
return FindBuiltInTarget(context, spv::BuiltIn::Position, IsVec4Float32, outTarget);
|
||||
}
|
||||
|
||||
// Put `variableId` back on `entryPoint`'s interface list if it is not already there.
|
||||
//
|
||||
// SPIR-V requires every Input/Output global an entry point statically uses to be listed on
|
||||
// its OpEntryPoint, and spirv-val enforces it ("Interface variable id <N> is used by entry
|
||||
// point 'main' id <M>, but is not listed as an interface"). The link-time sanitize chain
|
||||
// DELISTS a variable nothing referenced yet - ShaderCompiler::SanitizeAndOptimizeBinary
|
||||
// runs CreateAggressiveDCEPass(false), which may never delete an Output, followed by
|
||||
// CreateRemoveUnusedInterfaceVariablesPass, which rebuilds the operand list from the
|
||||
// variables actually referenced. A TES that redeclares `out gl_PerVertex { vec4
|
||||
// gl_Position; }` and never writes it therefore reaches the backend with the OpVariable
|
||||
// and its BuiltIn Position decoration intact and its interface slot gone. Any pass that
|
||||
// then injects a reference has to put the slot back, or it hands the driver a module no
|
||||
// validator accepts - and Mali r54 answers that with a SIGSEGV inside pipeline creation
|
||||
// rather than an error return.
|
||||
//
|
||||
// No SPIR-V version gate here, unlike GlFragCoordYFlipPass's identical call for its
|
||||
// injected PRIVATE global: Input and Output belong on the interface in every version,
|
||||
// and only 1.4 widened it to the other storage classes.
|
||||
Bool EnsureEntryPointInterface(spvtools::opt::IRContext* context, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 variableId) {
|
||||
// In-operands: 0 = execution model, 1 = entry function id, 2 = name, 3.. = interface.
|
||||
constexpr Uint32 kFirstInterfaceOperand = 3;
|
||||
if (variableId == 0) return false;
|
||||
for (Uint32 operand = kFirstInterfaceOperand; operand < entryPoint.NumInOperands(); ++operand) {
|
||||
if (entryPoint.GetSingleWordInOperand(operand) == variableId) return false;
|
||||
}
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
|
||||
context->AnalyzeUses(&entryPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is `pointerId` the position target itself, or an access chain rooted at it?
|
||||
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
|
||||
const PositionTargetInfo& target) {
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
for (Uint32 current = pointerId; current != 0;) {
|
||||
if (current == target.variableId) return true;
|
||||
const auto* inst = defUse->GetDef(current);
|
||||
if (inst == nullptr) return false;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpPtrAccessChain:
|
||||
case spv::Op::OpInBoundsPtrAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
current = inst->GetSingleWordInOperand(0);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does anything in the module write the position target?
|
||||
//
|
||||
// Deliberately conservative - it answers "assume yes" for every shape it cannot read
|
||||
// exactly, because a false "no" would silently drop the clip-space fixup from a shader
|
||||
// that does write gl_Position, while a false "yes" only reinstates the behaviour this
|
||||
// pass has always had. Scans every function rather than just the entry point's: a shader
|
||||
// that assigns gl_Position inside a helper is still a shader that writes it, and passing
|
||||
// the pointer to a call is a write as far as this can tell.
|
||||
Bool ModuleWritesPositionTarget(spvtools::opt::IRContext* context, const PositionTargetInfo& target) {
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (const auto& inst : block) {
|
||||
switch (inst.opcode()) {
|
||||
case spv::Op::OpStore:
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(0), target)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpFunctionCall:
|
||||
// In-operand 0 is the callee; the rest are arguments.
|
||||
for (Uint32 argument = 1; argument < inst.NumInOperands(); ++argument) {
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(argument),
|
||||
target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -911,6 +1070,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
|
||||
|
||||
// Nothing to remap in a Position the shader never writes. Declining is not just
|
||||
// an optimisation: the fixup is load-modify-store, so on an unwritten Position it
|
||||
// converts "undefined, never written" into "written with whatever the load
|
||||
// returned", and the store is a reference to a variable the link-time sanitize
|
||||
// chain has already delisted from the entry-point interface. glslang emits the
|
||||
// OpVariable for every DECLARED interface block, so a redeclared-but-unwritten
|
||||
// `out gl_PerVertex` is a shape real shaders have.
|
||||
if (!ModuleWritesPositionTarget(context(), target)) {
|
||||
MGLOG_D("gl-to-vulkan-position-fix: the shader never writes gl_Position; leaving it alone");
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
|
||||
if (!floatType) return Status::SuccessWithoutChange;
|
||||
|
||||
@@ -942,6 +1113,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
|
||||
if (!function) continue;
|
||||
|
||||
Bool modifiedThisEntryPoint = false;
|
||||
for (auto& bb : *function) {
|
||||
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
|
||||
auto* inst = &*instIter;
|
||||
@@ -950,10 +1122,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
|
||||
if (!needsFixup) continue;
|
||||
|
||||
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
modifiedThisEntryPoint |=
|
||||
InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
}
|
||||
}
|
||||
// Per entry point, and only for one this pass actually injected into: the
|
||||
// injected load/store is a static use of the position variable, so the
|
||||
// variable has to be on THIS entry point's interface list.
|
||||
if (modifiedThisEntryPoint) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
modified |= modifiedThisEntryPoint;
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1232,6 +1412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool needsPositionMirror = false;
|
||||
Uint32 positionBufferIndex = 0;
|
||||
Uint32 positionOffset = 0;
|
||||
// gl_PointSize is a gl_PerVertex MEMBER, never a variable of its own, so the
|
||||
// debug-name lookup below can never resolve it - it used to fall through to
|
||||
// "no SPIR-V variable named 'gl_PointSize'" and leave the frontend's reserved
|
||||
// slot unwritten, or, when it was the only capture, leave the module with no
|
||||
// Xfb execution mode at all and the whole span declined.
|
||||
Bool needsPointSizeMirror = false;
|
||||
Uint32 pointSizeBufferIndex = 0;
|
||||
Uint32 pointSizeOffset = 0;
|
||||
for (const auto& varying : m_varyings) {
|
||||
if (varying.name == "gl_Position") {
|
||||
needsPositionMirror = true;
|
||||
@@ -1239,6 +1427,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
positionOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.name == "gl_PointSize") {
|
||||
// A demoted module (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
|
||||
// built-in member - the value lives in the carrier variable the demotion
|
||||
// named - so the capture binds to the carrier directly. The mirror below
|
||||
// must not run for it: reading the now-unwritten member would capture
|
||||
// garbage, and the read itself is the capability access the demotion
|
||||
// exists to remove. Detected off the module's own debug names, so a
|
||||
// composite built from another program's stage answers for the module it
|
||||
// actually contains.
|
||||
const auto carrierIt = idsByName.find(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
|
||||
if (carrierIt != idsByName.end()) {
|
||||
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
continue;
|
||||
}
|
||||
needsPointSizeMirror = true;
|
||||
pointSizeBufferIndex = varying.bufferIndex;
|
||||
pointSizeOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.blockMemberIndex >= 0) {
|
||||
// glslang names the block's instance variable and its struct type
|
||||
// separately; an anonymous instance leaves only the type named, so
|
||||
@@ -1303,8 +1513,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::Position, IsVec4Float32,
|
||||
"gl_Position", positionBufferIndex, positionOffset,
|
||||
decorateForXfb);
|
||||
}
|
||||
if (needsPointSizeMirror) {
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::PointSize, IsFloat32Scalar,
|
||||
"gl_PointSize", pointSizeBufferIndex,
|
||||
pointSizeOffset, decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1344,19 +1562,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// gl_Position and gl_PointSize are captured the same way and differ only in which
|
||||
// built-in is looked up and what type it has, so one injector serves both. Anything
|
||||
// else in gl_PerVertex would need its own type check before it could be added here.
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||
Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
|
||||
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
|
||||
const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
|
||||
if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: %s capture requested but no such output found", glslName);
|
||||
return false;
|
||||
}
|
||||
if (!target.isMember) {
|
||||
// Standalone gl_Position variable: decorate it directly.
|
||||
// Standalone built-in variable: decorate it directly. It still has to be
|
||||
// on the interface - a transform-feedback decoration on a variable the entry
|
||||
// point does not list captures nothing, and the sanitize chain delists an
|
||||
// unwritten one (see EnsureEntryPointInterface).
|
||||
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1410,6 +1637,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
injected = true;
|
||||
}
|
||||
}
|
||||
// The mirror was listed on the entry point above, but the loop just added a READ
|
||||
// of the SOURCE block through an access chain, and the interface rule covers
|
||||
// reads exactly as it covers writes. A built-in capture on a shader whose
|
||||
// block the sanitize chain delisted - a TES that redeclares `out gl_PerVertex`
|
||||
// and never writes it, which is what the tessellation_control_to_tessellation_
|
||||
// evaluation.gl_MaxPatchVertices_Position_PointSize bodies do - produced an
|
||||
// invalid module here for the same reason the position fixup did.
|
||||
if (injected) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
|
||||
@@ -3233,11 +3470,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
if (program.PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
|
||||
// rendering assertion above it stays green on a healthy driver whether the
|
||||
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
|
||||
"demoted to an ordinary varying, because this device cannot host the built-in "
|
||||
"in those stages.");
|
||||
}
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
if (enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
||||
// not only when the switch is armed, so the validator's static tables have to be pinned
|
||||
// against process exit in every build too.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
||||
|
||||
@@ -3261,6 +3508,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
|
||||
// These two passes INJECT references - a store for the clip fixup, an access
|
||||
// chain and a load for the gl_Position capture mirror - and a reference to a
|
||||
// variable the link-time sanitize chain delisted from the entry-point interface
|
||||
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
|
||||
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
|
||||
// is the backstop.
|
||||
//
|
||||
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
|
||||
// are not equally optional. Rewinding straight to `spv` would also throw away the
|
||||
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
|
||||
// Xfb execution mode - while the renderer decides to call
|
||||
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
|
||||
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
|
||||
// into a transform-feedback span, violating
|
||||
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
|
||||
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
|
||||
// capture working and costs only the clip-space remap.
|
||||
//
|
||||
// Once per program on a cache miss, and only for the single stage that carries the
|
||||
// fixups - not per draw and not per module.
|
||||
SpirvValidationFailure fixupFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
|
||||
&fixupFailure)) {
|
||||
SpirvValidationFailure xfbFailure{};
|
||||
if (fixupInput != &spv &&
|
||||
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the capture-decorated one, so this program draws "
|
||||
"without the clip-space remap",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = *fixupInput;
|
||||
} else {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the untransformed one",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
@@ -3469,15 +3756,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only
|
||||
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
|
||||
// by here the descriptor bindings have been remapped and the layout about to be
|
||||
// reflected describes the remapped module, so there is no module left that is both
|
||||
// valid and consistent with it to fall back to. The recovery lives one step earlier,
|
||||
// at the clip/XFB fixups (see the revert there) - which is where a transform can
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
// varying and changed nothing, anything later that might strip it. The renderer picks
|
||||
// its capture commands from GL state alone and would otherwise open a span against a
|
||||
// pipeline that cannot feed it.
|
||||
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0 &&
|
||||
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
|
||||
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
|
||||
"than recorded against a pipeline that cannot feed them",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.xfbCaptureDeclined = true;
|
||||
}
|
||||
|
||||
// Does this stage need a device feature the device did not give us? Asked ONLY when
|
||||
// the feature is off, so a device that has it - the common case - pays nothing: the
|
||||
// whole test is short-circuited before the module is parsed.
|
||||
//
|
||||
// gl_PointSize is an ordinary per-vertex output in desktop GL and any
|
||||
// vertex-processing stage may write it, but Vulkan puts the built-in behind
|
||||
// shaderTessellationAndGeometryPointSize in the tessellation and geometry stages
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). glslang emits TessellationPointSize /
|
||||
// GeometryPointSize from the application's own access, so this program is legal GL
|
||||
// that this device cannot run - the same shape the DirectGLES arm reports when a
|
||||
// driver advertises neither EXT nor OES point-size extension, and it deserves the
|
||||
// same named message rather than a pipeline the driver may fault on.
|
||||
if (!m_tessellationAndGeometryPointSizeEnabled &&
|
||||
(stages[i] == ShaderStage::TessControl || stages[i] == ShaderStage::TessEval ||
|
||||
stages[i] == ShaderStage::Geometry) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(
|
||||
moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u stage %d accesses gl_PointSize, but this device does not "
|
||||
"support shaderTessellationAndGeometryPointSize; its draws are refused rather than "
|
||||
"built into a pipeline the driver may fault on. Point size from a non-vertex stage "
|
||||
"is not available on this device.",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.pointSizeCapabilityUnsupported = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = moduleSpv.size() * sizeof(Uint);
|
||||
@@ -3594,18 +3929,136 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
|
||||
Uint64 ProgramFactory::ComputePassthroughTessControlKey(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// A plain 32-byte blob of exactly what the generator reads, hashed once. Deliberately over
|
||||
// the RAW BITS rather than the values: two levels that compare unequal must key apart, and
|
||||
// a NaN level - which glPatchParameterfv accepts - compares unequal to itself.
|
||||
struct Blob {
|
||||
Uint32 patchVertices;
|
||||
Uint32 outerBits[4];
|
||||
Uint32 innerBits[2];
|
||||
Uint32 perVertexMembers;
|
||||
} blob{};
|
||||
blob.patchVertices = patchVertices;
|
||||
for (Uint32 i = 0; i < 4; ++i) blob.outerBits[i] = std::bit_cast<Uint32>(defaultOuterLevel[i]);
|
||||
for (Uint32 i = 0; i < 2; ++i) blob.innerBits[i] = std::bit_cast<Uint32>(defaultInnerLevel[i]);
|
||||
blob.perVertexMembers = perVertexMembers;
|
||||
return XXH64(&blob, sizeof(blob), 0);
|
||||
}
|
||||
|
||||
// The member list a gl_PerVertex redeclaration must spell, derived from the mask. Order is
|
||||
// glslang's declaration order and is load-bearing: a redeclaration whose members are the same
|
||||
// set in a different order is a different block.
|
||||
static String BuildPerVertexMemberDeclarations(Uint32 perVertexMembers) {
|
||||
using Bit = ProgramFactory::PerVertexMemberBit;
|
||||
String members;
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::Position)) members += " vec4 gl_Position;\n";
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::PointSize)) members += " float gl_PointSize;\n";
|
||||
// Sized at one, not left unsized: an unsized built-in array in a redeclared block is
|
||||
// implicitly sized by use, and this stage never indexes either distance array.
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::ClipDistance)) members += " float gl_ClipDistance[1];\n";
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::CullDistance)) members += " float gl_CullDistance[1];\n";
|
||||
return members;
|
||||
}
|
||||
|
||||
Uint32 ProgramFactory::ReflectPerVertexInputMembers(const Vector<Uint>& spirv) {
|
||||
// Minimal, self-contained SPIR-V walk. SPIRV-Reflect is deliberately NOT used: for an
|
||||
// array of interface blocks it reports built_in == -1 on the block and leaves every
|
||||
// member's built_in at 0 (which is SpvBuiltInPosition), so a member walk through it reads
|
||||
// "Position, Position, Position" - the same trap ReflectPassthroughTessControlNeed
|
||||
// documents. The decorations below are unambiguous.
|
||||
constexpr SizeT kHeaderWords = 5;
|
||||
constexpr Uint32 kOpName = 5;
|
||||
constexpr Uint32 kOpDecorate = 71;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kOpTypeArray = 28;
|
||||
constexpr Uint32 kOpTypePointer = 32;
|
||||
constexpr Uint32 kOpVariable = 59;
|
||||
constexpr Uint32 kDecorationBlock = 2;
|
||||
constexpr Uint32 kDecorationBuiltIn = 11;
|
||||
constexpr Uint32 kStorageClassInput = 1;
|
||||
constexpr Uint32 kBuiltInPosition = 0;
|
||||
constexpr Uint32 kBuiltInPointSize = 1;
|
||||
constexpr Uint32 kBuiltInClipDistance = 3;
|
||||
constexpr Uint32 kBuiltInCullDistance = 4;
|
||||
(void)kOpName;
|
||||
|
||||
if (spirv.size() <= kHeaderWords) return 0;
|
||||
|
||||
UnorderedMap<Uint32, Uint32> arrayElementType; // array id -> element type id
|
||||
UnorderedMap<Uint32, Pair<Uint32, Uint32>> pointerPointee; // pointer id -> (storage class, pointee)
|
||||
UnorderedMap<Uint32, Uint32> structMembers; // struct id -> PerVertexMemberBit mask
|
||||
std::set<Uint32> blockStructs;
|
||||
Vector<Uint32> inputVariablePointerTypes;
|
||||
|
||||
for (SizeT i = kHeaderWords; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
const Uint32* words = &spirv[i];
|
||||
switch (opcode) {
|
||||
case kOpTypeArray:
|
||||
if (wordCount >= 4) arrayElementType[words[1]] = words[2];
|
||||
break;
|
||||
case kOpTypePointer:
|
||||
if (wordCount >= 4) pointerPointee[words[1]] = {words[2], words[3]};
|
||||
break;
|
||||
case kOpVariable:
|
||||
if (wordCount >= 4 && words[3] == kStorageClassInput) inputVariablePointerTypes.push_back(words[1]);
|
||||
break;
|
||||
case kOpDecorate:
|
||||
if (wordCount >= 3 && words[2] == kDecorationBlock) blockStructs.insert(words[1]);
|
||||
break;
|
||||
case kOpMemberDecorate:
|
||||
if (wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
Uint32 bit = 0;
|
||||
switch (words[4]) {
|
||||
case kBuiltInPosition: bit = static_cast<Uint32>(PerVertexMemberBit::Position); break;
|
||||
case kBuiltInPointSize: bit = static_cast<Uint32>(PerVertexMemberBit::PointSize); break;
|
||||
case kBuiltInClipDistance: bit = static_cast<Uint32>(PerVertexMemberBit::ClipDistance); break;
|
||||
case kBuiltInCullDistance: bit = static_cast<Uint32>(PerVertexMemberBit::CullDistance); break;
|
||||
default: break;
|
||||
}
|
||||
structMembers[words[1]] |= bit;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
|
||||
// The one Input variable whose type is an array of a Block-decorated struct IS gl_in;
|
||||
// gl_TessCoord and friends are plain scalars/vectors and never match.
|
||||
for (const Uint32 pointerType : inputVariablePointerTypes) {
|
||||
const auto pointer = pointerPointee.find(pointerType);
|
||||
if (pointer == pointerPointee.end()) continue;
|
||||
const auto array = arrayElementType.find(pointer->second.second);
|
||||
if (array == arrayElementType.end()) continue;
|
||||
if (!blockStructs.contains(array->second)) continue;
|
||||
const auto members = structMembers.find(array->second);
|
||||
if (members == structMembers.end()) continue;
|
||||
return members->second;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
|
||||
// control shader: "the input patch is passed through unmodified", the output patch has
|
||||
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Those two levels default to 1.0 and are baked here as literals because
|
||||
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
|
||||
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
|
||||
// making the levels a parameter of this source AND of the cache key in
|
||||
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
|
||||
// together here.
|
||||
// Those two levels are baked in as literals - Vulkan has no equivalent dynamic state, so
|
||||
// compiling them in is the only way to honour glPatchParameterfv. That makes them part of
|
||||
// this module's identity: GetOrCreatePassthroughTessControlStage keys its cache on them,
|
||||
// and PipelineFactory hashes them into the pipeline key. The three must move together.
|
||||
//
|
||||
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
|
||||
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
|
||||
@@ -3619,60 +4072,104 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// this from having to know the domain.
|
||||
String source = "#version 450 core\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
|
||||
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
|
||||
// matches built-in interface blocks by their whole shape, and the two obvious spellings
|
||||
// are both wrong:
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the NEIGHBOURING EVALUATION
|
||||
// STAGE carries, because Vulkan matches built-in interface blocks by their whole shape,
|
||||
// and the two obvious spellings are both wrong:
|
||||
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
|
||||
// zeroes (degenerate triangles, nothing rasterized), and
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
|
||||
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
|
||||
// vertex+evaluation program through this same compiler and fails if the two shapes ever
|
||||
// stop agreeing, rather than letting the mismatch show up as a black frame.
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields whatever THIS
|
||||
// source's #version implies, which is unrelated to the evaluation stage's.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
source += "in gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_out[];\n";
|
||||
// The member set is a PARAMETER rather than a constant, and that is the whole point: it
|
||||
// was hardcoded to {gl_Position, gl_PointSize, gl_ClipDistance[1]}, which is the shape a
|
||||
// program carries only below #version 450. glslang appends gl_CullDistance to the block
|
||||
// from 450 upward, so every 450/460 program - and every ESSL program, which the source
|
||||
// processor rewrites to "#version 460 core" - carried FOUR members against this stage's
|
||||
// three and got the black-frame-no-error case described above. The mask comes from
|
||||
// ReflectPerVertexInputMembers, read off the evaluation stage's own SPIR-V.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
|
||||
// links the program at both 430 and 460.
|
||||
//
|
||||
// Only gl_Position is written, and gl_PointSize is declared without being forwarded. That
|
||||
// is a KNOWN GAP, not a design: GL 4.6 core 11.2.2 says the fixed-function pass-through
|
||||
// hands the input patch to the evaluation stage unmodified, so an evaluation stage
|
||||
// reading gl_in[].gl_PointSize should see the vertex stage's value and instead sees
|
||||
// whatever this stage left in gl_out[] - which is nothing. A capture of it (the mirror in
|
||||
// XfbCaptureDecoratePass) faithfully records that nothing.
|
||||
//
|
||||
// The reason this comment used to give - "the renderer does not enable
|
||||
// shaderTessellationAndGeometryPointSize" - stopped being true when
|
||||
// VulkanRenderer::CreateLogicalDeviceAndQueues started taking the feature wherever the
|
||||
// device advertises it. Closing the gap is therefore possible now, but it is not free:
|
||||
// the forwarding store has to be gated on that feature, because on a device without it
|
||||
// the store is exactly the invalid usage the build-time refusal
|
||||
// (VkProgramObject::pointSizeCapabilityUnsupported) exists to keep away from the driver -
|
||||
// and this synthesized stage is not the application's, so refusing the program because
|
||||
// MobileGL's own pass-through named a built-in would be the wrong trade. Nothing pins
|
||||
// the shape either: every case in TessellationXfbCaptureScenario builds an explicit
|
||||
// control stage, so a TES-without-TCS test has to come with the fix.
|
||||
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
|
||||
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
|
||||
source += "void main() {\n";
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
for (Uint32 i = 0; i < 4; ++i) {
|
||||
source += " gl_TessLevelOuter[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
|
||||
}
|
||||
for (Uint32 i = 0; i < 2; ++i) {
|
||||
source += " gl_TessLevelInner[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
|
||||
}
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(
|
||||
Uint32 patchVertices, const FloatVec4& defaultOuterLevel, const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// Everything compiled into the stage, folded into one key. The patch size alone stopped
|
||||
// being enough once glPatchParameterfv could change the levels: two modules that differ
|
||||
// only in a baked-in level are different modules, and pipelines built from either may be
|
||||
// alive at the same time. The gl_PerVertex member set joins it for the same reason - two
|
||||
// programs at different GLSL versions need differently-shaped blocks.
|
||||
const Uint64 key =
|
||||
ComputePassthroughTessControlKey(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
|
||||
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
|
||||
// generator that cannot compile from re-running glslang on every draw.
|
||||
const auto cached = m_passthroughTessControlStages.find(patchVertices);
|
||||
const auto cached = m_passthroughTessControlStages.find(key);
|
||||
if (cached != m_passthroughTessControlStages.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
// The key stopped being bounded when the levels joined it: patchVertices alone could only
|
||||
// take 32 values, but six unclamped application floats can take any number, and an
|
||||
// application that ramps a level per frame would retain one VkShaderModule per frame for
|
||||
// the lifetime of the device. Flushed wholesale rather than aged: a module is not
|
||||
// referenced by the pipelines built from it (Vulkan copies what it needs at
|
||||
// vkCreateGraphicsPipelines), everything here runs on the GL thread, and an application
|
||||
// that can overflow this cap is already recompiling every frame - so the flush costs it
|
||||
// nothing it was not paying anyway.
|
||||
if (m_passthroughTessControlStages.size() >= kMaxPassthroughTessControlStages) {
|
||||
MGLOG_D("ProgramFactory: flushing %zu pass-through tessellation control stages; the application has "
|
||||
"used more than %zu distinct (patch size, default level) combinations",
|
||||
m_passthroughTessControlStages.size(), kMaxPassthroughTessControlStages);
|
||||
for (auto& entry : m_passthroughTessControlStages) {
|
||||
if (entry.second.module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
|
||||
}
|
||||
}
|
||||
m_passthroughTessControlStages.clear();
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||
stage.module = VK_NULL_HANDLE;
|
||||
stage.pName = "main";
|
||||
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = BuildPassthroughTessControlSource(patchVertices);
|
||||
const String source =
|
||||
BuildPassthroughTessControlSource(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
|
||||
// Same compile configuration as every other stage of every other program: this runs on
|
||||
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
|
||||
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
|
||||
@@ -3686,7 +4183,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
|
||||
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
|
||||
patchVertices, compiled.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3696,7 +4193,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!linked) {
|
||||
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
|
||||
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3705,19 +4202,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!binary || binary.value().empty() || binary.value().front().empty()) {
|
||||
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
|
||||
"for patchVertices=%u", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
{
|
||||
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
|
||||
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
|
||||
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
|
||||
// shipping build can be handed by an application. The message is latched all the same
|
||||
// - the pass-through cache is keyed on patchVertices, so a broken template would
|
||||
// otherwise re-report once per distinct patch size.
|
||||
Bool validateThisOne = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
validateThisOne = true;
|
||||
#else
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
validateThisOne = m_enableSpirvValidation;
|
||||
if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
#endif
|
||||
SpirvValidationFailure passthroughFailure{};
|
||||
if (validateThisOne &&
|
||||
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
|
||||
"patchVertices=%u does not validate (%s)",
|
||||
patchVertices, passthroughFailure.message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
@@ -3727,14 +4238,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
|
||||
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
stage.module = module;
|
||||
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
|
||||
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3744,6 +4255,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.needsPassthroughTessControl = false;
|
||||
entry.passthroughTessControlEmulatable = false;
|
||||
entry.passthroughPerVertexMembers = 0;
|
||||
|
||||
Bool hasTessEval = false;
|
||||
Bool hasTessControl = false;
|
||||
@@ -3763,6 +4275,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
|
||||
const auto& module = spirv[tessEvalModuleIndex];
|
||||
|
||||
// The shape the synthesized control stage has to redeclare. Read here because this is the
|
||||
// only place that holds the evaluation stage's module; a zero mask means the walk found
|
||||
// no input per-vertex block at all, in which case the pre-450 shape is the safe stand-in
|
||||
// (it is what every program carried before gl_CullDistance joined the block).
|
||||
const Uint32 perVertexMembers = ReflectPerVertexInputMembers(module);
|
||||
entry.passthroughPerVertexMembers = perVertexMembers != 0 ? perVertexMembers : kDefaultPerVertexMembers;
|
||||
if (perVertexMembers == 0) {
|
||||
MGLOG_W("ProgramFactory: could not read the evaluation stage's gl_PerVertex block shape; the "
|
||||
"pass-through control stage falls back to the pre-450 three-member form");
|
||||
}
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
|
||||
@@ -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) {
|
||||
@@ -485,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 {
|
||||
@@ -539,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;
|
||||
@@ -556,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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject1D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
@@ -37,6 +39,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
|
||||
// id only has to stay clear of the application's, exactly like the sampled fallback's.
|
||||
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
|
||||
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
|
||||
// single-sampled fallback they cannot be reinterpreted into another domain at view time
|
||||
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
|
||||
// same reason as the two above - they must not collide with anything glGenTextures can
|
||||
// hand out.
|
||||
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
|
||||
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
|
||||
|
||||
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
|
||||
// do reaches one, so anything keyed on the GL object an application bound - image-unit
|
||||
// aliasing above all - has to leave them alone.
|
||||
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) return false;
|
||||
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
|
||||
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
|
||||
(index >= kFallbackMultisampleExternalIndexBase &&
|
||||
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
|
||||
}
|
||||
|
||||
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support
|
||||
// format for uniform texel buffers, storage texel buffers and storage images alike
|
||||
@@ -360,6 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_textureManager = nullptr;
|
||||
m_samplerManager = nullptr;
|
||||
m_fallbackTexture2D.reset();
|
||||
m_fallbackMultisampleTextures.clear();
|
||||
}
|
||||
|
||||
void UniformManager::BeginFrame(Uint32 frameIndex) {
|
||||
@@ -483,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// alive through the draw via GL binding state. Only the fallback path needs a SharedPtr to
|
||||
// keep the fallback texture alive for the rest of this call.
|
||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
||||
@@ -496,7 +517,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
// The binding's sampler class, read here rather than through the `numericDomain`
|
||||
// local further down (it is declared after this point): the multisample placeholder
|
||||
// has to be built in the class the shader will read it in.
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
|
||||
texture = fallbackHolder.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
@@ -529,7 +553,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!IsValidSampledImageLayout(resource->layout)) {
|
||||
auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
FramebufferAttachmentType attachmentType = FramebufferAttachmentType::None;
|
||||
Int attachmentLevel = 0;
|
||||
if (drawFbo &&
|
||||
@@ -756,7 +780,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
||||
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (effectiveSampler == nullptr) return false;
|
||||
@@ -786,7 +810,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
outTexture.reset();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTexture: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTexture: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSamplerTexture: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -795,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
@@ -811,7 +835,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, Uint32 element) {
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTextureRaw: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSamplerTextureRaw: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -821,7 +845,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
|
||||
// touching the refcount (no atomic inc/dec per binding per draw).
|
||||
@@ -966,7 +990,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView& outBufferView) {
|
||||
outBufferView = VK_NULL_HANDLE;
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: frame index out of range");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
@@ -990,7 +1014,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
|
||||
@@ -1126,7 +1150,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorBufferInfo& outBufferInfo) const {
|
||||
outBufferInfo = {};
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(),
|
||||
"ResolveStorageBufferDescriptor: binding %u out of range", binding);
|
||||
|
||||
@@ -1163,12 +1187,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? static_cast<GLuint>(atomicCounterBinding)
|
||||
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||
const Uint32 bindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
|
||||
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(bufferTarget));
|
||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
|
||||
frontendBinding, blockName.c_str());
|
||||
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
||||
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
|
||||
@@ -1240,7 +1264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorImageInfo& outImageInfo) const {
|
||||
outImageInfo = {};
|
||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageImageDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveStorageImageDescriptor: binding %u out of range", binding);
|
||||
|
||||
@@ -1269,7 +1293,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
// Legal GL: an image unit with no texture bound makes loads return zero and discards
|
||||
// stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning
|
||||
@@ -1367,18 +1391,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return outImageInfo.imageView != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
||||
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
|
||||
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
|
||||
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
|
||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||
// process over an unbound sampler is never the right answer.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// A multisample sampler cannot be served by the single-sampled 2D image below - its
|
||||
// descriptor demands a multisample view - so it gets its own placeholder rather than no
|
||||
// placeholder at all. Without one, ResolveSamplerDescriptor declined and
|
||||
// BindProgramUniformBuffers dropped the WHOLE draw, which is how every
|
||||
// sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
|
||||
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
|
||||
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
|
||||
// incomplete texture is undefined, not fatal, so the draw has to happen.
|
||||
if (target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray) {
|
||||
return GetFallbackMultisampleTexture(target, numericDomain);
|
||||
}
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
|
||||
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
|
||||
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
|
||||
// image can never carry that bit, which is why the arm above needs one object per domain.
|
||||
if (m_fallbackTexture2D == nullptr) {
|
||||
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
||||
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
@@ -1396,6 +1432,83 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return m_fallbackTexture2D;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const {
|
||||
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
|
||||
//
|
||||
// A descriptor whose image format is in a different numeric class than the sampler that
|
||||
// reads it needs a format-reinterpreting view, and building one needs
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
|
||||
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
|
||||
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
|
||||
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
|
||||
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
|
||||
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
|
||||
// image's own format to the sampler's class instead means no reinterpreting view is
|
||||
// needed at all.
|
||||
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
|
||||
Uint32 domainSlot = 0;
|
||||
switch (numericDomain) {
|
||||
case SamplerNumericDomain::SignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8I;
|
||||
domainSlot = 1;
|
||||
break;
|
||||
case SamplerNumericDomain::UnsignedInteger:
|
||||
internalFormat = TextureInternalFormat::RGBA8UI;
|
||||
domainSlot = 2;
|
||||
break;
|
||||
case SamplerNumericDomain::Float:
|
||||
case SamplerNumericDomain::Unknown:
|
||||
default:
|
||||
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
|
||||
// a shader whose sampler class could not be reflected is far likelier to be a plain
|
||||
// sampler2DMS than an integer one, and a float view is the only one buildable without
|
||||
// the mutable bit anyway.
|
||||
break;
|
||||
}
|
||||
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
|
||||
auto cached = m_fallbackMultisampleTextures.find(key);
|
||||
if (cached != m_fallbackMultisampleTextures.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
|
||||
: TextureUploadTarget::Texture2DMultisample;
|
||||
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
|
||||
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
|
||||
if (arrayed) {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
|
||||
} else {
|
||||
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
|
||||
}
|
||||
texture->SetInternalFormat(internalFormat);
|
||||
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
|
||||
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
|
||||
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
|
||||
// that re-created it would be worse than none.
|
||||
texture->SetSamples(2);
|
||||
texture->SetFixedSampleLocations(true);
|
||||
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
|
||||
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
|
||||
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
|
||||
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
|
||||
// texture that is not complete. The point of the placeholder is that the DRAW happens.
|
||||
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
texture->MarkStorageDirty(uploadTarget, 0, false);
|
||||
// Worth knowing if it ever fires: an integer multisample format can legitimately support
|
||||
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
|
||||
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
|
||||
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
|
||||
// above exists to avoid. It already warns from there; nothing better is available - a
|
||||
// one-sample integer image is still a draw, and declining is the outcome this whole
|
||||
// placeholder replaced.
|
||||
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
|
||||
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
|
||||
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
|
||||
}
|
||||
|
||||
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
|
||||
SamplerNumericDomain numericDomain, Bool storage) {
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null");
|
||||
@@ -1536,7 +1649,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
|
||||
// texture and the sampler override - this runs per binding per full-path draw,
|
||||
// and program-alternating draw streams take the full path on every draw.
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSampledBinding: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSampledBinding: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSampledBinding: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -1547,29 +1660,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
// The sampler in effect, resolved BEFORE the completeness test below rather than after:
|
||||
// GL's completeness rules are a property of (texture, sampler in effect), so the test
|
||||
// cannot be asked without it.
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
const MG_State::GLState::SamplerObject* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get()
|
||||
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
|
||||
// like ResolveSamplerTextureRaw reports it.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||
texture = nullptr;
|
||||
}
|
||||
// ...and so does a texture that fails the completeness rules for the filter in effect,
|
||||
// because that is precisely what ResolveSamplerDescriptor does with it. The two used to
|
||||
// disagree: this one asked only whether the default texture was UNDEFINED, so a default
|
||||
// texture that had been given a base level but no mip chain - which is what the GL-CTS
|
||||
// state reset between test cases leaves behind, and what any application that uploads to
|
||||
// texture 0 has - stayed in the sampled set while the descriptor path swapped it for the
|
||||
// fallback. SetupDraw then synced a texture no descriptor would use, the sync declined
|
||||
// (GL calls it incomplete), and the null it returned was dereferenced one line later.
|
||||
// Keeping the two predicates identical is the invariant; CollectSampledTextures exists to
|
||||
// pre-sync exactly the textures the descriptors will hold.
|
||||
if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
// Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
|
||||
// a multisample arm and the two must not drift apart.
|
||||
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
// The substitution changed the texture, so the "no override" arm of the effective
|
||||
// sampler has to follow it to the fallback's own.
|
||||
if (!samplerOverride) {
|
||||
effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr;
|
||||
}
|
||||
}
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
outTexture = texture;
|
||||
outSampler = samplerOverride ? samplerOverride.get()
|
||||
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||
outSampler = effectiveSampler;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1667,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
|
||||
outTextures.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
||||
"CollectStorageImageTextures: GL context is null");
|
||||
// Same as the sampled walk: a declined program is refused at bind time, and its declined
|
||||
// binding has no uniform location to reach an image unit through.
|
||||
@@ -1711,7 +1848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
auto* texture = MGB_CTX->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
// ResolveStorageImageDescriptor will substitute the placeholder image for this
|
||||
// binding; include it here for the same reason the sampled walk includes the
|
||||
@@ -1749,7 +1886,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
||||
outBindings.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
||||
"CollectSamplerImageFeedback: GL context is null");
|
||||
if (programObj.declinedDescriptors) return true;
|
||||
|
||||
@@ -1765,9 +1902,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
||||
sampledTexture, sampledSampler) ||
|
||||
sampledTexture == nullptr || sampledSampler == nullptr ||
|
||||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
|
||||
IsPlaceholderTexture(sampledTexture)) {
|
||||
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
||||
// alias the image-unit binding of the original texture.
|
||||
// alias the image-unit binding of the original texture. The unbound and
|
||||
// incomplete cases both arrive here AS that fallback now that
|
||||
// ResolveSampledBinding applies the completeness rule itself, so the test is
|
||||
// "is this one of ours" rather than a second completeness check.
|
||||
continue;
|
||||
}
|
||||
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
||||
@@ -1796,7 +1936,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
return false;
|
||||
}
|
||||
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& image = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
// A sampler view exposes all layers of its target; equal texture plus an
|
||||
// overlapping mip therefore aliases the writable image subresource.
|
||||
if (image.Texture.get() == sampledTexture &&
|
||||
@@ -1826,7 +1966,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* outData = nullptr;
|
||||
VkDeviceSize outSize = 0;
|
||||
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveUniformBufferPayload: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveUniformBufferPayload: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.bindingKinds.size(),
|
||||
"ResolveUniformBufferPayload: binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(programObj.bindingKinds[binding] == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic,
|
||||
@@ -1871,12 +2011,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast<Uint32>(blockIndex));
|
||||
const Uint32 uniformBindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));
|
||||
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(BufferTarget::Uniform));
|
||||
MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount,
|
||||
"ResolveUniformBufferPayload: frontend UBO binding %u out of range for block '%s'",
|
||||
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
||||
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
||||
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
MOBILEGL_ASSERT(bufferObject != nullptr,
|
||||
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
|
||||
@@ -1938,6 +2078,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
out.dynamicOffset = rangeStart;
|
||||
}
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
|
||||
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
|
||||
// payload a split build would have to ship with set_shader_buffers. Espryt binds
|
||||
// the frontend buffer to the driver and contributes nothing here, which is why
|
||||
// the class is named for the payload and not for the call. Counted AFTER the
|
||||
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
|
||||
// here reported a copy that never happened.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
|
||||
static_cast<Uint64>(outSize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2145,6 +2295,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outBuffer = slice.buffer;
|
||||
outRange = ubo.payloadSize;
|
||||
outDynamicOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo && MG_Util::PipeStats::Enabled()) {
|
||||
// Magma's half of stage-ubo-global, so the class means the same on both
|
||||
// backends. The memo hit above returns before this, so a frame that reuses the
|
||||
// slice correctly contributes nothing.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
||||
static_cast<Uint64>(ubo.payloadSize));
|
||||
}
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] =
|
||||
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
|
||||
@@ -179,7 +179,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
@@ -293,6 +303,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
|
||||
@@ -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,6 +10,8 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
@@ -229,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() {
|
||||
@@ -339,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;
|
||||
}
|
||||
@@ -353,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;
|
||||
@@ -422,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,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;
|
||||
}
|
||||
@@ -484,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;
|
||||
}
|
||||
@@ -554,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;
|
||||
@@ -602,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;
|
||||
}
|
||||
|
||||
@@ -681,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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
@@ -86,25 +87,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
}
|
||||
|
||||
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the
|
||||
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this
|
||||
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a
|
||||
// geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's
|
||||
// upper layers were never written at all.
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, 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
|
||||
@@ -112,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;
|
||||
@@ -334,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),
|
||||
@@ -618,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])));
|
||||
@@ -772,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
|
||||
@@ -858,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,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();
|
||||
@@ -970,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;
|
||||
@@ -1011,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;
|
||||
@@ -1100,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 {
|
||||
@@ -1122,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());
|
||||
@@ -1216,8 +1227,12 @@ 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;
|
||||
@@ -1229,8 +1244,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} 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;
|
||||
@@ -1304,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();
|
||||
@@ -1327,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();
|
||||
@@ -1424,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;
|
||||
@@ -1438,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,
|
||||
@@ -1464,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;
|
||||
@@ -380,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:
|
||||
@@ -812,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 {
|
||||
@@ -921,18 +916,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
@@ -945,7 +950,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat baseAttachmentFormat =
|
||||
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
|
||||
const VkFormat attachmentFormat =
|
||||
@@ -2173,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;
|
||||
@@ -3143,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,37 @@ 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
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
@@ -424,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);
|
||||
}
|
||||
|
||||
@@ -432,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);
|
||||
}
|
||||
|
||||
@@ -441,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);
|
||||
}
|
||||
|
||||
@@ -450,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);
|
||||
}
|
||||
@@ -459,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);
|
||||
}
|
||||
|
||||
@@ -467,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);
|
||||
}
|
||||
|
||||
@@ -476,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);
|
||||
}
|
||||
|
||||
@@ -485,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);
|
||||
}
|
||||
|
||||
@@ -493,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);
|
||||
}
|
||||
|
||||
@@ -502,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);
|
||||
}
|
||||
@@ -512,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);
|
||||
}
|
||||
@@ -522,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);
|
||||
}
|
||||
@@ -532,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);
|
||||
}
|
||||
|
||||
@@ -542,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);
|
||||
}
|
||||
@@ -552,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);
|
||||
}
|
||||
@@ -562,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);
|
||||
}
|
||||
@@ -572,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);
|
||||
}
|
||||
|
||||
@@ -580,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,
|
||||
@@ -588,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);
|
||||
}
|
||||
@@ -597,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);
|
||||
}
|
||||
|
||||
@@ -605,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);
|
||||
}
|
||||
|
||||
@@ -636,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);
|
||||
}
|
||||
|
||||
@@ -688,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -709,10 +836,42 @@ 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
|
||||
@@ -748,9 +907,32 @@ 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;
|
||||
@@ -761,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);
|
||||
}
|
||||
@@ -851,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(
|
||||
@@ -872,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(
|
||||
@@ -887,7 +1070,7 @@ 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;
|
||||
@@ -897,7 +1080,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -905,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);
|
||||
@@ -914,7 +1097,7 @@ 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;
|
||||
@@ -925,21 +1108,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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;
|
||||
@@ -949,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);
|
||||
@@ -971,7 +1154,7 @@ 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;
|
||||
@@ -981,7 +1164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
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);
|
||||
@@ -989,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(
|
||||
@@ -1003,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);
|
||||
}
|
||||
@@ -1011,7 +1194,7 @@ 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;
|
||||
@@ -1035,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);
|
||||
@@ -1083,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);
|
||||
}
|
||||
}
|
||||
@@ -1165,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();
|
||||
@@ -1173,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);
|
||||
}
|
||||
}
|
||||
@@ -1194,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();
|
||||
}
|
||||
}
|
||||
@@ -1208,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();
|
||||
}
|
||||
}
|
||||
@@ -1413,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);
|
||||
@@ -1444,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);
|
||||
}
|
||||
}
|
||||
@@ -1459,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(
|
||||
@@ -1482,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,
|
||||
@@ -1501,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,
|
||||
|
||||
@@ -160,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)
|
||||
@@ -411,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)
|
||||
@@ -923,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)
|
||||
@@ -994,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)
|
||||
@@ -1107,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)
|
||||
@@ -1150,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)
|
||||
@@ -2049,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)
|
||||
@@ -2546,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,7 +7,9 @@
|
||||
// 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>
|
||||
@@ -28,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
|
||||
@@ -93,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;
|
||||
@@ -106,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
|
||||
@@ -134,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) {
|
||||
@@ -304,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,
|
||||
@@ -473,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;
|
||||
@@ -484,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;
|
||||
@@ -680,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;
|
||||
}
|
||||
@@ -735,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;
|
||||
@@ -800,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;
|
||||
@@ -815,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();
|
||||
@@ -1036,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);
|
||||
@@ -1186,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>(
|
||||
@@ -1209,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();
|
||||
}
|
||||
}
|
||||
@@ -1222,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]);
|
||||
}
|
||||
@@ -1237,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;
|
||||
@@ -1268,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:
|
||||
@@ -1275,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:
|
||||
@@ -1314,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;
|
||||
@@ -1369,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());
|
||||
@@ -1657,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;
|
||||
@@ -1710,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;
|
||||
@@ -1755,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);
|
||||
@@ -1815,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;
|
||||
@@ -1900,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;
|
||||
@@ -2011,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();
|
||||
}
|
||||
}
|
||||
@@ -2118,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);
|
||||
@@ -2174,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:
|
||||
@@ -2219,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:
|
||||
@@ -2242,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;
|
||||
@@ -2276,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;
|
||||
@@ -2287,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;
|
||||
@@ -2343,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;
|
||||
@@ -2360,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;
|
||||
|
||||
@@ -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);
|
||||
@@ -94,6 +96,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
MGP_FILL(ClientWaitSync);
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
@@ -119,6 +122,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
MGP_FILL(WaitSync);
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -139,6 +143,7 @@ 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;
|
||||
@@ -174,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;
|
||||
@@ -227,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);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,19 +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
|
||||
@@ -273,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()
|
||||
|
||||
@@ -338,7 +359,37 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
|
||||
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
|
||||
# MOBILEGL_LOG_FILE_PATH alongside the pin, because the arming assertion needs somewhere to
|
||||
# read the library's own report from. The strip's arming signal is a latched MGLOG_I and there
|
||||
# is no other way for a test process to learn that it fired - MG_Config is not reachable from
|
||||
# this module on Android, where it links the shipping library. The path is per-lane so nothing
|
||||
# else appends to it, and the case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
|
||||
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
|
||||
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
|
||||
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
|
||||
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
|
||||
# SPIR-V Xfb decorations to the carrier).
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
@@ -405,6 +456,23 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
|
||||
# reason the depth/stencil entry above pins its emulation: without it this scenario is
|
||||
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
|
||||
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
|
||||
# emulation never runs - the ambient registration would be exercising the un-stripped path
|
||||
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
|
||||
# location and the assertion is about the spelling the device gets.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
|
||||
TEST_FILTER "UnlocatedIoBlockScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||
#
|
||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||
@@ -501,3 +569,245 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
|
||||
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
|
||||
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
|
||||
# the same GL questions and must produce the same numbers - the "two pools must agree"
|
||||
# gate this machine can hold that the affected device cannot. The pinned entry is also
|
||||
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
|
||||
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
|
||||
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
|
||||
# steers nothing on DirectGLES.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PrimGenReroute."
|
||||
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
|
||||
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
|
||||
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
|
||||
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
|
||||
# DirectVulkan, which has always stored these formats widened.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.WidenedPacked16."
|
||||
TEST_FILTER "CopyImagePacked16Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
|
||||
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
|
||||
# geometry stages, so the ambient registrations run these captures through the built-in and
|
||||
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
|
||||
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
|
||||
# path. Both backends, because the demotion is shared phase-B work with two different
|
||||
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
|
||||
TEST_FILTER "PointSizeDemotionScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- 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
|
||||
@@ -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
|
||||
|
||||
@@ -225,4 +225,43 @@ void main() {
|
||||
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
||||
}
|
||||
|
||||
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
||||
// between. Each backend has its own way to invert this pair, and both are pinned here.
|
||||
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
|
||||
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
|
||||
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
|
||||
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
|
||||
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
|
||||
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
|
||||
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
|
||||
// the deferred frame command buffer. Unless the frontend retires that pending work before
|
||||
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
|
||||
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
|
||||
// pins the other direction for both: the upload must leave bytes outside its range - the
|
||||
// dispatch's results - untouched.
|
||||
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||
MakeCounterBuffer(1, {0u});
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
Dispatch();
|
||||
|
||||
const unsigned int reseed = 4242u;
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
|
||||
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), &reseed);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
|
||||
|
||||
const std::vector<unsigned int> values = ReadCounters(zero, 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(values[0], reseed)
|
||||
<< "offset 0 read back " << values[0] << "; the dispatch's value (" << kInvocations
|
||||
<< ") means the readback ran before the queued SubData range was flushed and reverted it";
|
||||
EXPECT_EQ(values[1], 2 * kInvocations)
|
||||
<< "offset 4 read back " << values[1] << "; the SubData flush must leave bytes outside its "
|
||||
<< "range untouched";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,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,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
@@ -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
|
||||
@@ -119,6 +119,73 @@ void main() {
|
||||
imageStore(u_unbound, int(index), uvec4(7u));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
|
||||
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
|
||||
// level and no mip chain - GL calls the second one incomplete for the initial
|
||||
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
|
||||
// texture the backend then fails to back.
|
||||
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texture(u_unbound, vec2(0.0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
|
||||
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
|
||||
// unused one at an empty unit, so whichever of the two is unused has to have a
|
||||
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
|
||||
// cannot stand in for it.
|
||||
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform sampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, ivec2(0), 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
||||
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
||||
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
||||
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
||||
// been created.
|
||||
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform usampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform isampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
@@ -369,6 +436,65 @@ void main() {
|
||||
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
||||
}
|
||||
|
||||
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
|
||||
}
|
||||
|
||||
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
|
||||
// unit whose DEFAULT texture object has an image but no mip chain.
|
||||
//
|
||||
// DirectVulkan resolved such a binding twice, through two different predicates that
|
||||
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
|
||||
// pre-syncs and transitions every texture the draw will sample, asked only whether the
|
||||
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
|
||||
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
|
||||
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
|
||||
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
|
||||
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
|
||||
// a SIGSEGV inside the draw, not a degraded picture.
|
||||
//
|
||||
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
|
||||
// texture a base level, so the FIRST case in a process survived and every later one with an
|
||||
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
|
||||
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
|
||||
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
|
||||
// object bound there a FORMAT and a zero-sized level - which is what a bare
|
||||
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
|
||||
// state reset issues for every texture target. That combination is the whole point:
|
||||
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
|
||||
// and the texture stays in the sampled set;
|
||||
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
|
||||
// true and the descriptor holds the fallback instead;
|
||||
// * and it has no valid mip level, so the sync declines and hands back nullptr.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
|
||||
// QUALIFIER REMOVED.
|
||||
//
|
||||
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
|
||||
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
|
||||
// stages compile, the program links with an empty info log, the draw runs, and the consuming
|
||||
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
|
||||
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
|
||||
// from the blocks (and changing nothing else) makes the same program carry its payload. The
|
||||
// locations are not the application's in the first place: these shaders declare none, and
|
||||
// glslang's cross-stage IO resolver invents them.
|
||||
//
|
||||
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
|
||||
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
|
||||
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
|
||||
// location must still deliver its payload end to end. It is the assertion the affected device
|
||||
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
|
||||
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
|
||||
// cannot reproduce the defect at all.
|
||||
//
|
||||
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
|
||||
// entry, because llvmpipe carries a located block correctly and the driver POST would
|
||||
// therefore never turn the emulation on here. The SAME cases also run under the ambient
|
||||
// registrations with the emulation off, so both spellings of the interface are covered and a
|
||||
// regression in either shows up.
|
||||
//
|
||||
// Colour code, so a failure names its own cause:
|
||||
// green - the payload crossed all four stage boundaries, which is the pass.
|
||||
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
||||
// backend program was rejected and every draw became a no-op).
|
||||
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
||||
// failure is not about interface blocks.
|
||||
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
||||
// zeroed. That is what an interface whose two ends stopped matching looks like.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// NOTHING in these five stages declares a location. Every location the emitted ESSL
|
||||
// carries is invented by the cross-stage resolver, which is exactly the shape the
|
||||
// affected driver mishandles and exactly what the strip removes.
|
||||
//
|
||||
// Two members per block, of different types, because an interface that is matched by
|
||||
// name and member sequence rather than by location has to agree on the sequence too -
|
||||
// a repair that silently reordered or dropped a member would still light up green with
|
||||
// one member in the block.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
out VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} vs_out;
|
||||
out float vs_tcs_alive;
|
||||
void main()
|
||||
{
|
||||
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
vs_out.tint = vec2(0.25, 0.5);
|
||||
vs_tcs_alive = 1.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
in VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_in[];
|
||||
in float vs_tcs_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_out[];
|
||||
out float tcs_tes_alive[];
|
||||
void main()
|
||||
{
|
||||
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
||||
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
|
||||
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Distinct block names, so this case is about the LOCATION and nothing else; the
|
||||
// one-name-in-both-directions shape is the case below.
|
||||
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// The 420pack shape: ONE name for the block this stage consumes and the block it
|
||||
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
|
||||
// the rename gives the two blocks one spelling per producing stage, the strip takes
|
||||
// their locations off, and the interfaces still have to meet.
|
||||
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// One geometry source per evaluation stage, because the block it consumes is named
|
||||
// after the block the evaluation stage produced.
|
||||
const char* const kDistinctGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kCollidingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// Green ONLY when both block members arrived: a repair that kept the first member and
|
||||
// lost the second would otherwise pass. Red when the plain varying is missing too, so
|
||||
// "the pipeline is broken" and "the block is broken" cannot be confused.
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
in GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} fs_in;
|
||||
in float gs_fs_alive;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
if (gs_fs_alive <= 0.5) {
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
|
||||
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
} else {
|
||||
fragColor = fs_in.payload;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
class UnlocatedIoBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!BackendHostsTessellationAndGeometry()) {
|
||||
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
||||
<< "carry a block through";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
|
||||
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
|
||||
// frontend constants, so a stack with no five-stage pipeline is recognised by
|
||||
// trying to build one, not by asking.
|
||||
static bool BackendHostsTessellationAndGeometry() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
||||
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
||||
geometrySource, kFragmentSource};
|
||||
|
||||
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
||||
bool ok = true;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
shaders[i] = glCreateShader(stages[i]);
|
||||
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
||||
glCompileShader(shaders[i]);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = InfoLog(shaders[i], true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) {
|
||||
if (shader != 0) glDeleteShader(shader);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (!linked) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
||||
// than something that could be mistaken for a zeroed payload.
|
||||
Rgba8 DrawAndReadCentre(GLuint program) const {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
return pixel;
|
||||
}
|
||||
|
||||
static bool IsGreen(const Rgba8& pixel) {
|
||||
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// The library log this process is writing, or an empty path when none was
|
||||
// configured. MOBILEGL_LOG_FILE_PATH is read at log-init, before anything this
|
||||
// fixture can reach, so the ctest entry sets it and this only reads it back.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// How many bytes the library log already holds. Everything this fixture asserts on
|
||||
// is searched from here forward, because the file is APPENDED to by every process
|
||||
// in the lane and a line left behind by an earlier one would otherwise satisfy the
|
||||
// assertion without this process having done anything at all.
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static GLenum FirstGLError() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
}
|
||||
return std::string(log.data());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
|
||||
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
|
||||
"member arrived wrong, i.e. the interface stopped matching)";
|
||||
}
|
||||
|
||||
// The two repairs together. The rename is what makes the evaluation stage's two
|
||||
// TcsData blocks one spelling per producing stage; the strip then takes the locations
|
||||
// off the names the rename just settled. Either one alone leaves a working program on
|
||||
// these machines, so this case is here to catch the two of them disagreeing.
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "an interface block name reused across the two directions of one stage is legal "
|
||||
"desktop GLSL, but the program did not build: "
|
||||
<< BuildLog();
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REPAIR SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above renders green on llvmpipe whether the blocks were stripped or not -
|
||||
// this machine carries a located block correctly - so those cases pin that the strip
|
||||
// does no HARM and can say nothing about whether it happened. That leaves the arming
|
||||
// itself untested, and the arming is where the cheap mistake lives: Loader.cpp maps
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS onto the capability INVERTED (forcing the
|
||||
// emulation on means declaring located blocks UNSUPPORTED), and a one-line swap of
|
||||
// those two arms would disable the device repair with every test here still green.
|
||||
//
|
||||
// So this case asserts a LIBRARY OBSERVABLE against the environment, the shape
|
||||
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration uses: the environment
|
||||
// says the emulation is pinned on, therefore the library must SAY it stripped
|
||||
// something. The observable is the latched MGLOG_I DirectGLES emits the first time the
|
||||
// pass fires (Managers.cpp); it is INFO rather than DEBUG precisely so that this
|
||||
// assertion is possible in the builds CI runs.
|
||||
//
|
||||
// Two things it deliberately does NOT do: it does not read MG_Config (on Android this
|
||||
// module links the shipping library, which exports nothing internal - the reason
|
||||
// ViewportArrayScenario's control moved to the environment), and it does not trust the
|
||||
// whole log file, only the bytes appended after this test started.
|
||||
TEST_F(UnlocatedIoBlockScenario, TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the emulation pinned ON for the whole process, which "
|
||||
"is what the UnlocatedIoBlocks. ctest entry does with "
|
||||
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1; with the variable unset the "
|
||||
"driver POST decides, and on this machine it decides the blocks are "
|
||||
"fine - so there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it stripped anything; the UnlocatedIoBlocks. ctest entry "
|
||||
"sets both";
|
||||
}
|
||||
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||
GTEST_SKIP() << "the strip is DirectGLES's; " << Gl().BackendName()
|
||||
<< " hands the module to the driver as SPIR-V, where Location is how "
|
||||
"interfaces are matched";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote. The latch means it is emitted at the FIRST stage of the
|
||||
// FIRST affected program, which is inside the build below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so nothing would arm the strip: " << BuildLog();
|
||||
}
|
||||
// Drawn as well as built, so a stack that defers its backend program to first use
|
||||
// still reaches the transpile this is asserting about.
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre)) << "the pinned-on lane did not even render correctly: " << centre;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("WITHOUT their layout(location) qualifier"), std::string::npos)
|
||||
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned ON, a five-stage program with four "
|
||||
"interface-block boundaries was built and drawn, and DirectGLES never reported "
|
||||
"stripping a single location. The emulation is not armed - check the override "
|
||||
"mapping in Loader.cpp (it is inverted on purpose) and the arming gate in "
|
||||
"Managers.cpp. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,276 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A SHADER REDECLARES gl_PerVertex AND NEVER WRITES gl_Position.
|
||||
//
|
||||
// Legal, ordinary GLSL, and until now a process kill on DirectVulkan. The chain, all of it
|
||||
// inside MobileGL's own SPIR-V plumbing:
|
||||
//
|
||||
// 1. glslang emits every DECLARED interface variable, used or not, and lists it on
|
||||
// OpEntryPoint. So `out gl_PerVertex { vec4 gl_Position; };` with no write still produces
|
||||
// the OpVariable, the OpMemberDecorate BuiltIn Position, and an interface slot.
|
||||
// 2. At link, ShaderCompiler::SanitizeAndOptimizeBinary runs AggressiveDCE(remove_outputs =
|
||||
// false) - which may never delete an Output - and then RemoveUnusedInterfaceVariables,
|
||||
// which rebuilds the interface list from the variables instructions actually reference.
|
||||
// The OpVariable and its BuiltIn decoration SURVIVE; the interface slot is DELISTED.
|
||||
// 3. At pipeline build, ProgramFactory picks the last pre-rasterisation stage and runs two
|
||||
// passes over it. GlToVulkanPositionFixPass finds the position target through the
|
||||
// surviving ANNOTATION and injects a load-modify-STORE through it. When gl_Position is in
|
||||
// the transform-feedback capture list, XfbCaptureDecoratePass::MirrorPositionForCapture
|
||||
// also injects an access chain and a LOAD through it.
|
||||
// 4. Either injection is a static use of a variable that is no longer on the entry point's
|
||||
// interface, which is invalid SPIR-V ("Interface variable id <N> is used by entry point
|
||||
// 'main' id <M>, but is not listed as an interface"). Mali r54 does not reject such a
|
||||
// module - it faults inside pipeline creation and takes the process down.
|
||||
//
|
||||
// Measured on a Mali-G1-Ultra as 216 KHR-GL44/45/46.tessellation_shader.tessellation_control_
|
||||
// to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize_* crashes; the CTS's TES
|
||||
// there is exactly the shape below. It is not tessellation-specific and not XFB-specific: a
|
||||
// vertex shader is enough, which is what these cases use.
|
||||
//
|
||||
// Every test captures a USER varying through transform feedback under GL_RASTERIZER_DISCARD.
|
||||
// Position is undefined in the first two by construction, so it is never asserted on - what is
|
||||
// asserted is that the capture came back at all, which it can only do if the driver accepted
|
||||
// the module and built a pipeline.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kCaptureFloats = 4;
|
||||
constexpr GLsizeiptr kCaptureBytes = static_cast<GLsizeiptr>(kCaptureFloats * sizeof(float));
|
||||
|
||||
// The defect's shape: gl_PerVertex redeclared, gl_Position never assigned.
|
||||
constexpr const char* kUnwrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The control that isolates the redeclaration: identical but for the one assignment.
|
||||
// This one keeps its interface slot through the sanitize chain, so both injections were
|
||||
// always legal on it - it must stay working.
|
||||
constexpr const char* kWrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The second control, and the one the CTS calls data_pass_through: no gl_PerVertex
|
||||
// redeclaration at all, so there is no Position annotation for the passes to find and
|
||||
// nothing to delist. It was never affected and proves the crash needs the redeclaration.
|
||||
constexpr const char* kNoPositionBlockVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
GLuint CompileVertexShader(const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// `captureNames` is what goes to glTransformFeedbackVaryings. Passing gl_Position in it
|
||||
// is what puts MirrorPositionForCapture on the path.
|
||||
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::string* log) {
|
||||
const GLuint vertexShader = CompileVertexShader(vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(captureNames.size()), captureNames.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
class UnwrittenPositionOutputScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const float vertex[kCaptureFloats] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, vertex, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// Links `vertexSource` with `captureNames`, runs one captured point, and checks that
|
||||
// the USER varying came back. `captureStride` is how many floats one captured vertex
|
||||
// occupies, so the user varying can be read out from behind a captured gl_Position.
|
||||
void ExpectUserVaryingIsCaptured(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::size_t captureStride, std::size_t userVaryingOffset,
|
||||
const char* what) {
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(vertexSource, captureNames, &log);
|
||||
ASSERT_NE(program, 0u) << what << ": the capture program failed to build: " << log;
|
||||
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(captureStride * sizeof(float));
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
// Pre-fill with a value the shader cannot produce, so "captured nothing" is
|
||||
// distinguishable from "captured the wrong thing".
|
||||
const std::vector<float> poison(captureStride, -1.0f);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, captureBytes, poison.data(), GL_DYNAMIC_READ);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << what << ": setting up the capture buffer raised a GL error";
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the captured draw raised a GL error";
|
||||
|
||||
std::vector<float> readback(captureStride, -2.0f);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, readback.data());
|
||||
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
|
||||
EXPECT_FLOAT_EQ(readback[userVaryingOffset + i], static_cast<float>(i + 1))
|
||||
<< what << ": captured float " << i << " came back as "
|
||||
<< readback[userVaryingOffset + i]
|
||||
<< "; the pre-fill value means the draw never produced a vertex, which is what an "
|
||||
"invalid shader module looks like from out here";
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The clip fixup's half: PositionZRemap is on for every draw, so the fixup runs on this
|
||||
// program and used to inject a store through the delisted block.
|
||||
TEST_F(UnwrittenPositionOutputScenario, ARedeclaredButUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared, never written");
|
||||
}
|
||||
|
||||
// The XFB half: capturing gl_Position adds an access chain and a LOAD through the same
|
||||
// delisted block, which the interface rule covers exactly as it covers the store. Position
|
||||
// itself is undefined here - only the user varying behind it is asserted.
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAnUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// DirectVulkan only, and not because the defect was backend-specific in principle - the
|
||||
// injection this pins lives in DirectVulkan's ProgramFactory, and DirectGLES cannot
|
||||
// reach the case at all: capturing gl_Position BY NAME off a shader that never writes it
|
||||
// comes back empty there, because the ESSL the transpiler emits has no such output for
|
||||
// the capture list to name. That is a known, separate DirectGLES gap (the same one that
|
||||
// blocks gl_Position/gl_PointSize capture in the tessellation capture segment), tracked
|
||||
// outside this scenario; asserting it here would only re-report it.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "capturing an unwritten gl_Position by name is a separate, known "
|
||||
<< "DirectGLES gap; this case pins the DirectVulkan injection";
|
||||
}
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing an unwritten gl_Position");
|
||||
}
|
||||
|
||||
// Control: the same shader with the one assignment restored. Its block is never delisted,
|
||||
// so it exercises the path the fixup is actually for and must keep working.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AWrittenRedeclaredPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared and written");
|
||||
}
|
||||
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAWrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing a written gl_Position");
|
||||
}
|
||||
|
||||
// Control: no gl_PerVertex redeclaration, so no Position annotation and nothing to delist.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AShaderWithNoPositionBlockStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kNoPositionBlockVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"no gl_PerVertex block");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -525,7 +525,7 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
//
|
||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
||||
// the SAME program in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// the SAME program in a process started with MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
|
||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
||||
@@ -551,10 +551,10 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
// entry for it, so the control still runs in every ctest run; anywhere else - the
|
||||
// ambient ctest entries, or the binary run straight from a device shell - the
|
||||
// emulation is on and this case skips.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the "
|
||||
"whole process; the NoViewportArrayEmulation. ctest entry runs it with "
|
||||
"MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
"MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
}
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A CAPTURE MUST STILL RECORD WHEN IT IS NOT THE FIRST ONE IN THE PROCESS,
|
||||
// AND THE CAPTURE STAGE MAY BE ANY OF THE FOUR THAT CAN BE THE LAST ONE.
|
||||
//
|
||||
// The conformance suite exposed a whole family of transform feedback failures that no
|
||||
// existing scenario could reproduce, because every one of them ran ONE capture, from a
|
||||
// VERTEX stage, in a freshly initialised process. What the suite actually does is
|
||||
// different in three ways at once, and each of them turned out to matter:
|
||||
//
|
||||
// * it runs case after case in ONE GL context, resetting state between them - and the
|
||||
// reset is not a fresh context. Its transform feedback part
|
||||
// (framework/opengl/gluStateReset.cpp resetStateGLCore) unbinds the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER and then clears every indexed capture point from 0 to
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, which permanently raises MobileGL's
|
||||
// touched-binding-point high-water mark. Every later capture that uses fewer points
|
||||
// than that - i.e. every INTERLEAVED_ATTRIBS capture - then had the unused tail
|
||||
// re-cleared on the driver immediately before glBeginTransformFeedback.
|
||||
// ReplayDeqpStateReset below is that reset, reduced to the calls that touch capture
|
||||
// state, so a defect that only appears from the second capture onwards is reachable
|
||||
// here instead of only on a device.
|
||||
//
|
||||
// * the capture stage is frequently a GEOMETRY or a TESSELLATION EVALUATION shader,
|
||||
// never a plain vertex shader. The tree had zero coverage for either: none of the
|
||||
// Xfb* scenarios mentioned tessellation and neither TessellationDrawModeScenario nor
|
||||
// GeometryDrawModeScenario mentioned transform feedback.
|
||||
//
|
||||
// * the capture program frequently has NO FRAGMENT STAGE at all, because it draws
|
||||
// under GL_RASTERIZER_DISCARD and never rasterises anything. That is legal in
|
||||
// desktop GL and the shape most "use transform feedback as a readback channel"
|
||||
// tests are built on.
|
||||
//
|
||||
// Every case here asserts the captured BYTES, never just the absence of a GL error: the
|
||||
// failure this guards against writes nothing and raises nothing, so a buffer that kept
|
||||
// its poison is the only thing that distinguishes it from success.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure ("the capture never reached these bytes") instead of looking
|
||||
// like an ordinary numeric mismatch.
|
||||
constexpr int kPoison = -987654;
|
||||
|
||||
const char* const kPassthroughVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in int vs_in_value;
|
||||
flat out int vs_out_value;
|
||||
void main()
|
||||
{
|
||||
vs_out_value = vs_in_value;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The primitive_counter shape: one flat int per emitted vertex, several vertices
|
||||
// per input primitive, so the capture is geometry-AMPLIFIED and the CPU-side
|
||||
// primitive model cannot predict its length.
|
||||
const char* const kPointAmplifyingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 2) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
gs_out_value = vs_out_value[0];
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Adjacency input. Only a geometry stage can consume it, and CountPrimitivesForDraw
|
||||
// used to answer 0 for every adjacency mode, which silently excluded the whole draw
|
||||
// from the capture accounting.
|
||||
const char* const kAdjacencyGeometrySource = R"(#version 420 core
|
||||
layout(lines_adjacency) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
gs_out_value = vs_out_value[1];
|
||||
gl_Position = gl_in[1].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
patch out int tcs_out_value;
|
||||
void main()
|
||||
{
|
||||
tcs_out_value = vs_out_value[0];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
patch in int tcs_out_value;
|
||||
flat out int tes_out_value;
|
||||
void main()
|
||||
{
|
||||
tes_out_value = tcs_out_value;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
flat in int gs_out_value;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(float(gs_out_value), 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class XfbRepeatedCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const int values[kInputVertices] = {10, 11, 12, 13};
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(values), values, GL_STATIC_DRAW);
|
||||
glVertexAttribIPointer(0, 1, GL_INT, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vbo = 0;
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static constexpr int kInputVertices = 4;
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 2;
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
// The transform-feedback-relevant half of deqp's resetStateGLCore, in its order.
|
||||
// It runs between EVERY pair of conformance cases, and running one capture
|
||||
// through it is the difference between "the first capture in the process" and
|
||||
// every other one.
|
||||
static void ReplayDeqpStateReset() {
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
GLint maxSeparateAttribs = 0;
|
||||
glGetIntegerv(GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSeparateAttribs);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
for (GLint index = 0; index < maxSeparateAttribs; ++index) {
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(index), 0);
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const char* varying) {
|
||||
return BuildCaptureProgram(stages, std::vector<const char*>{varying});
|
||||
}
|
||||
|
||||
// Builds a capture program out of `stages` capturing `varyings` interleaved.
|
||||
// Returns 0 and fills m_buildLog on failure.
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span. `captureMode` is the transform feedback primitive mode,
|
||||
// `drawMode`/`count` the draw. Returns the capture buffer's contents.
|
||||
std::vector<int> RunCaptureSpan(GLuint program, GLenum captureMode, GLenum drawMode, GLsizei count,
|
||||
std::size_t capturedInts) {
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// The capture point is the ONLY thing bound; the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER binding comes along for the ride, exactly as
|
||||
// the conformance tests rely on (GL 4.6 core 6.1.1).
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(drawMode, 0, count);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<int> readback(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
glUseProgram(0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedNothing(const std::vector<int>& data) {
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != kPoison) {
|
||||
return ::testing::AssertionFailure() << "component " << i << " is " << data[i];
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedIs(const std::vector<int>& data,
|
||||
const std::vector<int>& expected) {
|
||||
if (data.size() != expected.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "captured " << data.size() << " value(s), expected " << expected.size();
|
||||
}
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != expected[i]) {
|
||||
::testing::AssertionResult failure = ::testing::AssertionFailure();
|
||||
failure << "component " << i << " is " << data[i] << ", expected " << expected[i];
|
||||
if (data[i] == kPoison) {
|
||||
failure << " (the capture never reached these bytes)";
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// THE REGRESSION GUARD FOR THE WHOLE FAMILY. Two geometry-stage captures in one
|
||||
// process with the conformance suite's own state reset between them; the assertion
|
||||
// that matters is on the SECOND one, which is the one every device run failed while
|
||||
// whichever body happened to land first in its process passed.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASecondGeometryCaptureAfterADeqpStateResetStillRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
// Two vertices emitted per input point, so the capture is amplified beyond what
|
||||
// the CPU primitive model can predict from the draw alone.
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
|
||||
for (int capture = 0; capture < 3; ++capture) {
|
||||
// A fresh program per capture, because that is what a fresh conformance case
|
||||
// builds - and it is what makes the driver recycle program and buffer names.
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture " << capture << " program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected))
|
||||
<< "capture " << capture << " of 3 in this process"
|
||||
<< (capture == 0 ? "" : " (every earlier one was followed by a deqp-shaped state reset)");
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "capture " << capture;
|
||||
|
||||
glDeleteProgram(program);
|
||||
m_programs.pop_back();
|
||||
ReplayDeqpStateReset();
|
||||
glBindVertexArray(m_vao);
|
||||
}
|
||||
}
|
||||
|
||||
// The tessellation half, which had no coverage anywhere in the tree: a capture taken
|
||||
// from a GL_PATCHES draw, whose last vertex-processing stage is the evaluation shader
|
||||
// and whose record count only the tessellator knows.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAPatchesDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
|
||||
// One input patch of one vertex, all levels at 1: the tessellator emits exactly
|
||||
// one triangle, so three captured vertices all carrying the first input value.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
|
||||
"tes_out_value");
|
||||
ASSERT_NE(program, 0u) << "patch capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 10};
|
||||
const std::vector<int> captured = RunCaptureSpan(program, GL_TRIANGLES, GL_PATCHES, 1, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A capture program with NO FRAGMENT STAGE, drawn under GL_RASTERIZER_DISCARD. Legal
|
||||
// in desktop GL, and the shape most transform-feedback-as-readback tests use; the
|
||||
// program above only differs from it by the fragment shader, so a failure here is
|
||||
// specifically about the missing stage.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAFragmentlessProgramRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "fragmentless capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An ADJACENCY draw feeding the capture. CountPrimitivesForDraw answered 0 for all
|
||||
// four adjacency modes, which made the transform feedback accounting skip the draw
|
||||
// entirely - so neither the captured-vertex counter nor the geometry-capture-draw
|
||||
// flag moved, and anything downstream of either was working from "nothing happened".
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAnAdjacencyDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kAdjacencyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "adjacency capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line primitive; the shader emits
|
||||
// the second vertex of the four, which is the line's first real endpoint.
|
||||
const std::vector<int> expected = {11};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An adjacency draw with NO geometry stage. GL 4.6 core table 13.1 admits
|
||||
// GL_LINES_ADJACENCY and GL_LINE_STRIP_ADJACENCY under capture mode GL_LINES (and the
|
||||
// triangle pair under GL_TRIANGLES): without a geometry shader the adjacent vertices
|
||||
// are ignored and the primitive assembled is a plain line, so the combination is legal
|
||||
// and must capture. MobileGL's active-capture primitive-mode table listed only the
|
||||
// non-adjacency modes, so this raised GL_INVALID_OPERATION and dropped the draw
|
||||
// entirely - the buffer kept its pre-draw bytes and the application saw an error the
|
||||
// spec does not allow. Distinct from ACaptureFromAnAdjacencyDrawRecords above, which
|
||||
// HAS a geometry stage and therefore bypasses that table completely.
|
||||
TEST_F(XfbRepeatedCaptureScenario, AVertexOnlyAdjacencyCaptureRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(program, 0u) << "vertex-only capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line whose real endpoints are the
|
||||
// middle pair, so the capture is those two vertices in order.
|
||||
const std::vector<int> expected = {11, 12};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_LINES, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
|
||||
// THE GUARD FOR THE DEFECT ITSELF, and it is backend-independent: the frontend
|
||||
// validator must not reject the combination. It used to record
|
||||
// GL_INVALID_OPERATION and return before the draw was ever issued.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR)
|
||||
<< "a capture-mode/draw-mode pair GL 4.6 core table 13.1 admits must raise no error";
|
||||
|
||||
// Whether the capture then RECORDS is a backend question, and the two answer it
|
||||
// differently. ES 3.2 (10.1) supports the adjacency primitive types only for a
|
||||
// pipeline with a geometry shader, so DirectGLES has nothing to forward this draw
|
||||
// to; desktop GL and Vulkan both assemble the plain line and capture it. Asserting
|
||||
// the data unconditionally would be asserting that DirectGLES emulates a whole ES
|
||||
// restriction away, which is a separate piece of work and not what this guards.
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "DirectGLES cannot forward a geometry-shader-less adjacency draw: ES 3.2 10.1 "
|
||||
"supports the adjacency primitive types only with a geometry stage. The frontend "
|
||||
"no longer rejects the draw (checked above), which is the defect this covers.";
|
||||
}
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
}
|
||||
|
||||
// A CAPTURE MUST NEVER LAND IN A BUFFER THE APPLICATION DID NOT BIND FOR IT.
|
||||
//
|
||||
// A capture list may legally begin with gl_NextBuffer, which leaves capture buffer 0
|
||||
// with stride 0 and nothing to capture - so glBeginTransformFeedback does not require a
|
||||
// buffer at point 0 and the application binds only point 1. The driver-side program is
|
||||
// a single-buffer interleaved capture (the pseudo-varyings are consumed at link time),
|
||||
// so it writes capture point 0, and MobileGL redirects that into scratch storage and
|
||||
// scatters the records afterwards.
|
||||
//
|
||||
// Two ways that went wrong, both fixed here: the scratch was sized by reading each
|
||||
// target's stride at its POSITION in a list that skips unbound buffers, which for this
|
||||
// layout read stride 0 for everything and produced a zero capacity; and when the
|
||||
// scratch then failed to bind, the span opened anyway onto whatever capture point 0
|
||||
// still held from an earlier capture in the process - silently overwriting an unrelated
|
||||
// application buffer. The first span below exists purely to leave such a binding behind.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureListBeginningWithGlNextBufferSparesTheEarlierBuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::size_t capturedInts = 4;
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(capturedInts * sizeof(int));
|
||||
|
||||
// Span A: an ordinary capture, so capture point 0 is left holding bufferA.
|
||||
const GLuint programA =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(programA, 0u) << "plain capture program failed to build: " << m_buildLog;
|
||||
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint bufferA = 0;
|
||||
glGenBuffers(1, &bufferA);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, bufferA);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(programA);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> afterA(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, afterA.data());
|
||||
const std::vector<int> spanAExpected = {10, 11, 12, 13};
|
||||
ASSERT_TRUE(CapturedIs(afterA, spanAExpected)) << "the setup span itself did not capture";
|
||||
|
||||
// Span B: gl_NextBuffer first, so buffer 0 captures nothing and only point 1 is bound.
|
||||
const GLuint programB = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}},
|
||||
{"gl_NextBuffer", "vs_out_value"});
|
||||
if (programB == 0) {
|
||||
GTEST_SKIP() << "gl_NextBuffer capture lists are not linkable on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "): " << m_buildLog;
|
||||
}
|
||||
|
||||
GLuint bufferB = 0;
|
||||
glGenBuffers(1, &bufferB);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferB);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// Point 0 released, point 1 is the only destination this capture asks for.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, bufferB);
|
||||
|
||||
glUseProgram(programB);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
// THE ASSERTION THAT MATTERS: bufferA was not a destination of this capture, so it
|
||||
// must still read exactly what span A left in it. A failure here is the corruption.
|
||||
std::vector<int> bufferAAfterB(capturedInts, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, captureBytes, bufferAAfterB.data());
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
EXPECT_TRUE(CapturedIs(bufferAAfterB, spanAExpected))
|
||||
<< "the gl_NextBuffer capture wrote into the buffer the PREVIOUS span had bound";
|
||||
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and, where the backend places this layout at all, the buffer it WAS asked to
|
||||
// write gets the records. That placement is the DirectGLES scatter path, whose
|
||||
// scratch sizing used to read each target's stride at its POSITION in a list that
|
||||
// skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0
|
||||
// for every target and sized the scratch at zero. DirectVulkan does not implement a
|
||||
// leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a
|
||||
// pre-existing gap of its own, and the assertion above - that it corrupts nothing
|
||||
// while declining - is what matters for it.
|
||||
const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan";
|
||||
if (backendPlacesLeadingNextBuffer) {
|
||||
std::vector<int> bufferBAfter(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data());
|
||||
EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected));
|
||||
}
|
||||
|
||||
// Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer
|
||||
// this test deleted would follow the process into the next scenario.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0);
|
||||
glDeleteBuffers(1, &bufferA);
|
||||
glDeleteBuffers(1, &bufferB);
|
||||
|
||||
if (!backendPlacesLeadingNextBuffer) {
|
||||
GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it "
|
||||
"captures nothing, which the no-corruption assertion above has already covered.";
|
||||
}
|
||||
}
|
||||
|
||||
// The control for all of the above: a span that never draws must leave the capture
|
||||
// buffer alone. Without it "the buffer kept its poison" could be read as the correct
|
||||
// outcome of some path rather than as the bug, and the tightened early returns in
|
||||
// StartPendingTransformFeedback have to keep this legal case legal.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASpanThatNeverDrawsLeavesTheCaptureBufferAlone) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::size_t capturedInts = 8;
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glEndTransformFeedback();
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> readback(capturedInts, 0);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
EXPECT_TRUE(CapturedNothing(readback));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,131 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/Coverage.def
|
||||
// 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 hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call
|
||||
// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57
|
||||
// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py).
|
||||
//
|
||||
// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and
|
||||
// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc
|
||||
// with the per-accessor table and prints the coverage summary. Rows matching neither are
|
||||
// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes
|
||||
// "regenerate and git diff --exit-code with 0 UNMAPPED".
|
||||
//
|
||||
// Three pseudo-calls stand for read points that do NOT become a forward call:
|
||||
// kClientResolved - the frontend answers it itself; the server is never asked
|
||||
// (section 4.4.6: "the server answers nothing the client can answer").
|
||||
// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1).
|
||||
// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr<MG_State...>, which
|
||||
// becomes an MGPipeHandle parameter; there is no single call to name.
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
// X(Accessor, PipeCall)
|
||||
#define MGP_COVERAGE_ACCESSOR_LIST(X) \
|
||||
X(GetActiveTextureUnit, SetSamplerViews) \
|
||||
X(GetBlendColor, SetDynamicState) \
|
||||
X(GetBlendEquationIndexed, CreateRenderState) \
|
||||
X(GetBlendFuncIndexed, CreateRenderState) \
|
||||
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
|
||||
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
|
||||
X(GetBoundVertexArray, BindVertexElements) \
|
||||
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
|
||||
/* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \
|
||||
/* inventory is re-vendored carrying the target argument (deferred out of P1: */ \
|
||||
/* the extractor lives in MobileGL-CS). Named for the plan's explicit */ \
|
||||
/* replacement of the DrawIndirect/Parameter pair. */ \
|
||||
X(GetBufferBindingSlot, SetIndirectBuffers) \
|
||||
X(GetBufferBindingPoint, SetShaderBuffers) \
|
||||
X(GetBufferBindingPointCount, SetShaderBuffers) \
|
||||
X(GetTouchedBufferBindingPointCount, SetShaderBuffers) \
|
||||
X(GetClampReadColor, SetDynamicState) \
|
||||
X(GetClearColor, SetDynamicState) \
|
||||
X(GetClearDepth, SetDynamicState) \
|
||||
X(GetClearStencil, SetDynamicState) \
|
||||
X(GetColorMaskIndexed, CreateRenderState) \
|
||||
X(GetCullFaceMode, CreateRenderState) \
|
||||
X(GetCurrentVertexAttribute, SetVertexAttribDefaults) \
|
||||
X(GetDepthFunc, CreateRenderState) \
|
||||
X(GetDepthMask, CreateRenderState) \
|
||||
X(GetDepthRangeIndexed, SetDynamicState) \
|
||||
X(GetFramebufferBindingSlot, SetFramebufferState) \
|
||||
X(GetImageTextureBinding, SetShaderImages) \
|
||||
X(GetLineWidth, SetDynamicState) \
|
||||
X(GetLogicOp, CreateRenderState) \
|
||||
X(GetMaxTouchedTextureUnit, SetSamplerViews) \
|
||||
X(GetMinSampleShadingValue, CreateRenderState) \
|
||||
X(GetPatchDefaultInnerLevel, SetPatchState) \
|
||||
X(GetPatchDefaultOuterLevel, SetPatchState) \
|
||||
X(GetPatchVertices, SetPatchState) \
|
||||
X(GetPipelineStateVersion, BindRenderState) \
|
||||
X(GetPixelStoreParameters, SetPixelPackState) \
|
||||
X(GetPolygonModeFront, CreateRenderState) \
|
||||
X(GetPolygonOffsetFactor, SetDynamicState) \
|
||||
X(GetPolygonOffsetUnits, SetDynamicState) \
|
||||
X(GetPrimitiveRestartIndex, DrawVbo) \
|
||||
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||
X(GetProgramForDraw, SetDrawProgram) \
|
||||
X(GetProgramObject, CreateShaderState) \
|
||||
/* Not in ComputePipelineStateHash today even though Vulkan makes it pipeline */ \
|
||||
/* state; recorded here so the G7 chunk table has to answer for it before it */ \
|
||||
/* freezes (section 10.3-5). */ \
|
||||
X(GetProvokingVertexMode, CreateRenderState) \
|
||||
X(GetRenderStateParameters, CreateRenderState) \
|
||||
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||
X(GetSamplingResolutionGeneration, SetSamplerViews) \
|
||||
X(GetScissorBox, SetDynamicState) \
|
||||
X(GetStencilState, CreateRenderState) \
|
||||
X(GetTextureBindGeneration, SetSamplerViews) \
|
||||
X(GetTextureContextId, SetSamplerViews) \
|
||||
X(GetTextureObject, SetSamplerViews) \
|
||||
X(GetTextureUnitObject, SetSamplerViews) \
|
||||
X(GetTransformFeedbackCapturedVertices, DrawVbo) \
|
||||
X(GetTransformFeedbackGeneration, SetStreamOutputTargets) \
|
||||
X(GetTransformFeedbackPausedPrimitiveCounter, EndStreamOutput) \
|
||||
X(GetTransformFeedbackProgram, SetStreamOutputTargets) \
|
||||
X(GetViewport, SetDynamicState) \
|
||||
X(GetViewportIndexed, SetDynamicState) \
|
||||
X(IsCapabilityEnabled, CreateRenderState) \
|
||||
X(IsCapabilityEnabledIndexed, CreateRenderState) \
|
||||
X(IsTransformFeedbackActive, BeginStreamOutput) \
|
||||
X(IsTransformFeedbackPaused, PauseStreamOutput) \
|
||||
X(InvalidateCompileEnv, kClientResolved) \
|
||||
X(ValidateProgramName, kClientResolved) \
|
||||
X(RecordError, kReverseChannel) \
|
||||
/* The D21 XFB counter-slot rekey's reads (VulkanRenderer.cpp); the calls they */ \
|
||||
/* map to are GetTransformFeedbackGeneration's. */ \
|
||||
X(GetBoundTransformFeedbackLifetimeId, SetStreamOutputTargets) \
|
||||
X(HasOpenTransformFeedbackSpan, SetStreamOutputTargets)
|
||||
|
||||
// X(Accessor, Reason) - the STICKY fields (P1 brief D6): the only PipeInputs fields whose
|
||||
// value is valid across verbs, so the poison's per-verb generation does not apply to them.
|
||||
// Exactly the seven F-class (forwarded) accessors, and the argument for each is the same:
|
||||
// it 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. None of the version/generation accessors is sticky:
|
||||
// those change under verbs and are precisely what the poison must protect. The verify
|
||||
// lane's Fatal{UnmigratedPipeInput} is fixed by a FillPoints.def row, never by a row here.
|
||||
// gen_pipe.py refuses a name that is not an accessor above.
|
||||
#define MGP_COVERAGE_STICKY_LIST(X) \
|
||||
X(GetBufferBindingPointCount, "keyed by target: a constexpr capacity table, not verb state") \
|
||||
X(GetProgramObject, "keyed by GL name: an object lookup, not verb state") \
|
||||
X(GetTextureObject, "keyed by GL name: an object lookup, not verb state") \
|
||||
X(HasOpenTransformFeedbackSpan, "keyed by lifetime id: an object lookup, not verb state") \
|
||||
X(ValidateProgramName, "keyed by GL name: a name-table lookup, not verb state") \
|
||||
X(InvalidateCompileEnv, "reverse channel: a write into the frontend, not a state read") \
|
||||
X(RecordError, "reverse channel: a write into the frontend, not a state read")
|
||||
|
||||
// X(DeltaKind, PipeCall) - for inventory rows with no accessor in the member column.
|
||||
// Read by gen_pipe.py ONLY, never by the C++ preprocessor: the delta kinds are the
|
||||
// inventory's own free-text labels, not C tokens.
|
||||
#define MGP_COVERAGE_DELTA_LIST(X) \
|
||||
X(handle-ify (wire handle), kStructuralHandle) \
|
||||
X(Buffer ops delta, ResourceRespecify)
|
||||
|
||||
// clang-format on
|
||||
@@ -0,0 +1,296 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/FillPoints.def
|
||||
// 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 per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the
|
||||
// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into
|
||||
// generated/PipeFillPoints.inc:
|
||||
//
|
||||
// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class
|
||||
// MGP_FILL_CLASS_LIST the verb classes
|
||||
// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read
|
||||
//
|
||||
// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable
|
||||
// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and
|
||||
// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum
|
||||
// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every
|
||||
// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go
|
||||
// through BackendObject virtuals and read no frontend state, so they are not verbs here.
|
||||
//
|
||||
// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every
|
||||
// class and are not listed. The verify lane is the oracle for this table: a
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
|
||||
// row, never by marking the field sticky.
|
||||
//
|
||||
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
// X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h),
|
||||
// in declaration order. gen_pipe.py parses that struct and refuses a row set that is not exactly its member set.
|
||||
#define MGP_FILL_VERB_LIST(X) \
|
||||
X(DrawArrays, kDraw) \
|
||||
X(DrawElements, kDraw) \
|
||||
X(DrawElementsBaseVertex, kDraw) \
|
||||
X(MultiDrawArrays, kDraw) \
|
||||
X(MultiDrawElements, kDraw) \
|
||||
X(MultiDrawElementsBaseVertex, kDraw) \
|
||||
X(MultiDrawElementsIndirect, kDraw) \
|
||||
X(MultiDrawArraysIndirect, kDraw) \
|
||||
X(MultiDrawElementsIndirectCount, kDraw) \
|
||||
X(MultiDrawArraysIndirectCount, kDraw) \
|
||||
X(DrawRangeElementsBaseVertex, kDraw) \
|
||||
X(DrawRangeElements, kDraw) \
|
||||
X(DrawElementsInstancedBaseVertexBaseInstance, kDraw) \
|
||||
X(DrawElementsInstancedBaseVertex, kDraw) \
|
||||
X(DrawElementsInstancedBaseInstance, kDraw) \
|
||||
X(DrawElementsInstanced, kDraw) \
|
||||
X(DrawArraysInstancedBaseInstance, kDraw) \
|
||||
X(DrawArraysInstanced, kDraw) \
|
||||
X(DrawElementsIndirect, kDraw) \
|
||||
X(DrawArraysIndirect, kDraw) \
|
||||
X(Clear, kClear) \
|
||||
X(ClearBufferfi, kClear) \
|
||||
X(ClearBufferfv, kClear) \
|
||||
X(ClearBufferuiv, kClear) \
|
||||
X(ClearBufferiv, kClear) \
|
||||
X(ClearNamedFramebufferfv, kClear) \
|
||||
X(ClearNamedFramebufferfi, kClear) \
|
||||
X(ClearNamedFramebufferiv, kClear) \
|
||||
X(ClearNamedFramebufferuiv, kClear) \
|
||||
X(BlitFramebuffer, kBlitOrCopy) \
|
||||
X(BlitNamedFramebuffer, kBlitOrCopy) \
|
||||
X(CopyTexImage2D, kBlitOrCopy) \
|
||||
X(CopyTexSubImage2D, kBlitOrCopy) \
|
||||
X(CopyImageSubData, kBlitOrCopy) \
|
||||
X(GenerateMipmap, kTextureOp) \
|
||||
X(ReadPixels, kReadback) \
|
||||
X(GetTexImage, kReadback) \
|
||||
X(GetTextureImage, kReadback) \
|
||||
X(DispatchCompute, kDispatch) \
|
||||
X(DispatchComputeIndirect, kDispatch) \
|
||||
X(MemoryBarrier, kQuery) \
|
||||
X(MemoryBarrierByRegion, kQuery) \
|
||||
X(BindImageTexture, kTextureOp) \
|
||||
X(GetIntegeri_v, kQuery) \
|
||||
X(ShaderStorageBlockBinding, kProgramOp) \
|
||||
X(FenceSync, kQuery) \
|
||||
X(ClientWaitSync, kQuery) \
|
||||
X(WaitSync, kQuery) \
|
||||
X(DeleteSync, kQuery) \
|
||||
X(GetSyncStatus, kQuery) \
|
||||
X(IsTimerQuerySupported, kQuery) \
|
||||
X(BeginTimeElapsedQuery, kQuery) \
|
||||
X(EndTimeElapsedQuery, kQuery) \
|
||||
X(QueryCounterTimestamp, kQuery) \
|
||||
X(IsQueryResultAvailable, kQuery) \
|
||||
X(GetQueryResult64, kQuery) \
|
||||
X(DeleteBackendQuery, kQuery) \
|
||||
X(BeginOcclusionQuery, kQuery) \
|
||||
X(EndOcclusionQuery, kQuery) \
|
||||
X(BeginXfbPrimitivesQuery, kQuery) \
|
||||
X(EndXfbPrimitivesQuery, kQuery) \
|
||||
X(PatchParameteri, kQuery) \
|
||||
X(BeginTransformFeedback, kXfbSpan) \
|
||||
X(EndTransformFeedback, kXfbSpan) \
|
||||
X(PauseTransformFeedback, kXfbSpan) \
|
||||
X(ResumeTransformFeedback, kXfbSpan) \
|
||||
X(BindTransformFeedback, kXfbSpan) \
|
||||
X(DeleteTransformFeedback, kXfbSpan) \
|
||||
X(GetGpuTimestampNs, kQuery)
|
||||
|
||||
// X(Class) - the nine verb classes (ARCHITECTURE.md:153 names eight; kProgramOp is split out because
|
||||
// ShaderStorageBlockBinding is the one non-draw verb that syncs Espryt's render state and textures).
|
||||
#define MGP_FILL_CLASS_LIST(X) \
|
||||
X(kDraw) X(kDispatch) X(kClear) X(kBlitOrCopy) X(kTextureOp) X(kReadback) X(kXfbSpan) X(kProgramOp) X(kQuery)
|
||||
|
||||
// X(Class, Field) - the may-read table. A field named here is filled and stamped at every verb of the class;
|
||||
// a read of a field NOT named here is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
|
||||
// Derived from the verified reachability of every backend read (both backends, union), P1 brief D7.
|
||||
#define MGP_FILL_FIELD_LIST(X) \
|
||||
/* kDraw: every draw entry of both backends */ \
|
||||
X(kDraw, GetBoundVertexArray) \
|
||||
X(kDraw, GetProgramForDraw) \
|
||||
X(kDraw, GetBufferBindingSlot) \
|
||||
X(kDraw, GetBufferBindingPoint) \
|
||||
X(kDraw, GetTouchedBufferBindingPointCount) \
|
||||
X(kDraw, GetTextureUnitObject) \
|
||||
X(kDraw, GetTextureContextId) \
|
||||
X(kDraw, GetTextureBindGeneration) \
|
||||
X(kDraw, GetMaxTouchedTextureUnit) \
|
||||
X(kDraw, GetSamplingResolutionGeneration) \
|
||||
X(kDraw, GetImageTextureBinding) \
|
||||
X(kDraw, GetCurrentVertexAttribute) \
|
||||
X(kDraw, GetRenderStateParameters) \
|
||||
X(kDraw, GetRenderStateParametersVersion) \
|
||||
X(kDraw, GetPipelineStateVersion) \
|
||||
X(kDraw, GetViewport) \
|
||||
X(kDraw, GetViewportIndexed) \
|
||||
X(kDraw, GetDepthRangeIndexed) \
|
||||
X(kDraw, GetScissorBox) \
|
||||
X(kDraw, IsCapabilityEnabled) \
|
||||
X(kDraw, IsCapabilityEnabledIndexed) \
|
||||
X(kDraw, GetBlendColor) \
|
||||
X(kDraw, GetBlendFuncIndexed) \
|
||||
X(kDraw, GetBlendEquationIndexed) \
|
||||
X(kDraw, GetColorMaskIndexed) \
|
||||
X(kDraw, GetLogicOp) \
|
||||
X(kDraw, GetDepthFunc) \
|
||||
X(kDraw, GetDepthMask) \
|
||||
X(kDraw, GetStencilState) \
|
||||
X(kDraw, GetCullFaceMode) \
|
||||
X(kDraw, GetPolygonModeFront) \
|
||||
X(kDraw, GetPolygonOffsetFactor) \
|
||||
X(kDraw, GetPolygonOffsetUnits) \
|
||||
X(kDraw, GetLineWidth) \
|
||||
X(kDraw, GetMinSampleShadingValue) \
|
||||
X(kDraw, GetProvokingVertexMode) \
|
||||
X(kDraw, GetPatchVertices) \
|
||||
X(kDraw, GetPatchDefaultOuterLevel) \
|
||||
X(kDraw, GetPatchDefaultInnerLevel) \
|
||||
X(kDraw, GetPrimitiveRestartIndex) \
|
||||
X(kDraw, GetFramebufferBindingSlot) \
|
||||
X(kDraw, IsTransformFeedbackActive) \
|
||||
X(kDraw, IsTransformFeedbackPaused) \
|
||||
X(kDraw, GetTransformFeedbackProgram) \
|
||||
X(kDraw, GetTransformFeedbackGeneration) \
|
||||
X(kDraw, GetBoundTransformFeedbackLifetimeId) \
|
||||
X(kDraw, GetTransformFeedbackCapturedVertices) \
|
||||
/* kDispatch: the patch fields are Espryt's SyncCurrentProgram -> */ \
|
||||
/* AttachPassthroughTessControlStage (Managers.cpp) */ \
|
||||
X(kDispatch, GetProgramForDispatch) \
|
||||
X(kDispatch, GetBufferBindingSlot) \
|
||||
X(kDispatch, GetBufferBindingPoint) \
|
||||
X(kDispatch, GetTouchedBufferBindingPointCount) \
|
||||
X(kDispatch, GetTextureUnitObject) \
|
||||
X(kDispatch, GetTextureContextId) \
|
||||
X(kDispatch, GetTextureBindGeneration) \
|
||||
X(kDispatch, GetMaxTouchedTextureUnit) \
|
||||
X(kDispatch, GetSamplingResolutionGeneration) \
|
||||
X(kDispatch, GetImageTextureBinding) \
|
||||
X(kDispatch, GetFramebufferBindingSlot) \
|
||||
/* Magma's PrepareStorageImageTextures materialises a queued clear for every */ \
|
||||
/* storage image the dispatch writes, and the clear pre-compensates its colour */ \
|
||||
/* against GL_FRAMEBUFFER_SRGB (VkClearManager::PreCompensateSrgbClearColor). */ \
|
||||
X(kDispatch, IsCapabilityEnabled) \
|
||||
X(kDispatch, GetPatchVertices) \
|
||||
X(kDispatch, GetPatchDefaultOuterLevel) \
|
||||
X(kDispatch, GetPatchDefaultInnerLevel) \
|
||||
/* kClear */ \
|
||||
X(kClear, GetRenderStateParameters) \
|
||||
X(kClear, GetRenderStateParametersVersion) \
|
||||
X(kClear, GetViewport) \
|
||||
X(kClear, IsCapabilityEnabled) \
|
||||
X(kClear, GetFramebufferBindingSlot) \
|
||||
X(kClear, GetClearColor) \
|
||||
X(kClear, GetClearDepth) \
|
||||
X(kClear, GetClearStencil) \
|
||||
X(kClear, GetScissorBox) \
|
||||
X(kClear, GetColorMaskIndexed) \
|
||||
X(kClear, GetDepthMask) \
|
||||
X(kClear, GetStencilState) \
|
||||
X(kClear, GetTextureUnitObject) \
|
||||
X(kClear, GetTextureContextId) \
|
||||
X(kClear, GetSamplingResolutionGeneration) \
|
||||
X(kClear, GetTextureBindGeneration) \
|
||||
X(kClear, GetMaxTouchedTextureUnit) \
|
||||
X(kClear, GetImageTextureBinding) \
|
||||
/* kBlitOrCopy */ \
|
||||
X(kBlitOrCopy, GetFramebufferBindingSlot) \
|
||||
X(kBlitOrCopy, IsCapabilityEnabled) \
|
||||
X(kBlitOrCopy, GetScissorBox) \
|
||||
X(kBlitOrCopy, IsTransformFeedbackActive) \
|
||||
X(kBlitOrCopy, IsTransformFeedbackPaused) \
|
||||
X(kBlitOrCopy, GetRenderStateParameters) \
|
||||
X(kBlitOrCopy, GetRenderStateParametersVersion) \
|
||||
X(kBlitOrCopy, GetViewport) \
|
||||
X(kBlitOrCopy, GetActiveTextureUnit) \
|
||||
X(kBlitOrCopy, GetTextureUnitObject) \
|
||||
X(kBlitOrCopy, GetTextureContextId) \
|
||||
X(kBlitOrCopy, GetSamplingResolutionGeneration) \
|
||||
X(kBlitOrCopy, GetTextureBindGeneration) \
|
||||
X(kBlitOrCopy, GetMaxTouchedTextureUnit) \
|
||||
X(kBlitOrCopy, GetImageTextureBinding) \
|
||||
X(kBlitOrCopy, GetColorMaskIndexed) \
|
||||
X(kBlitOrCopy, GetDepthMask) \
|
||||
X(kBlitOrCopy, GetStencilState) \
|
||||
/* Magma's shader blit to the default framebuffer */ \
|
||||
/* (TryBlitToDefaultFramebufferWithShader) is a real draw of a backend-owned */ \
|
||||
/* helper program: it sets the dynamic viewport through ApplyGLViewportState */ \
|
||||
/* -> ComputeGLViewport (viewport 0 and its depth range), picks the pipeline's */ \
|
||||
/* provoking vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode, */ \
|
||||
/* and binds the helper's descriptors through BindProgramUniformBuffers, whose */ \
|
||||
/* buffer-block resolvers read the frontend binding points. */ \
|
||||
X(kBlitOrCopy, GetViewportIndexed) \
|
||||
X(kBlitOrCopy, GetDepthRangeIndexed) \
|
||||
X(kBlitOrCopy, GetProvokingVertexMode) \
|
||||
X(kBlitOrCopy, GetBufferBindingPoint) \
|
||||
/* kTextureOp */ \
|
||||
X(kTextureOp, GetActiveTextureUnit) \
|
||||
X(kTextureOp, GetTextureUnitObject) \
|
||||
X(kTextureOp, GetImageTextureBinding) \
|
||||
X(kTextureOp, GetTextureContextId) \
|
||||
X(kTextureOp, GetSamplingResolutionGeneration) \
|
||||
X(kTextureOp, GetTextureBindGeneration) \
|
||||
X(kTextureOp, GetMaxTouchedTextureUnit) \
|
||||
/* Magma's GenerateMipmap materialises the texture's queued clear before it */ \
|
||||
/* blits (MaterializePendingClearForTexture -> PreCompensateSrgbClearColor, */ \
|
||||
/* which reads GL_FRAMEBUFFER_SRGB), and a depth texture takes the shader path */ \
|
||||
/* (GenerateDepthMipmapWithShader -> BindProgramUniformBuffers), whose sampler */ \
|
||||
/* resolver reads the draw framebuffer for the feedback-loop check and whose */ \
|
||||
/* buffer-block resolvers read the frontend binding points. */ \
|
||||
X(kTextureOp, IsCapabilityEnabled) \
|
||||
X(kTextureOp, GetFramebufferBindingSlot) \
|
||||
X(kTextureOp, GetBufferBindingPoint) \
|
||||
/* kReadback */ \
|
||||
X(kReadback, GetPixelStoreParameters) \
|
||||
X(kReadback, GetBufferBindingSlot) \
|
||||
X(kReadback, GetFramebufferBindingSlot) \
|
||||
X(kReadback, GetActiveTextureUnit) \
|
||||
X(kReadback, GetTextureUnitObject) \
|
||||
X(kReadback, GetClampReadColor) \
|
||||
X(kReadback, IsCapabilityEnabled) \
|
||||
X(kReadback, GetRenderStateParameters) \
|
||||
X(kReadback, GetRenderStateParametersVersion) \
|
||||
X(kReadback, GetViewport) \
|
||||
X(kReadback, GetTextureContextId) \
|
||||
X(kReadback, GetSamplingResolutionGeneration) \
|
||||
X(kReadback, GetTextureBindGeneration) \
|
||||
X(kReadback, GetMaxTouchedTextureUnit) \
|
||||
X(kReadback, GetImageTextureBinding) \
|
||||
/* The depth/stencil read emulation draws (ScopedEmulationDrawState, */ \
|
||||
/* DirectGLES.cpp) and pauses an active capture around its own draw, so a */ \
|
||||
/* readback reads the transform-feedback state exactly as a draw does. */ \
|
||||
X(kReadback, IsTransformFeedbackActive) \
|
||||
X(kReadback, IsTransformFeedbackPaused) \
|
||||
/* kXfbSpan */ \
|
||||
X(kXfbSpan, GetTransformFeedbackProgram) \
|
||||
X(kXfbSpan, GetBufferBindingPoint) \
|
||||
X(kXfbSpan, GetTouchedBufferBindingPointCount) \
|
||||
X(kXfbSpan, GetTransformFeedbackCapturedVertices) \
|
||||
X(kXfbSpan, IsTransformFeedbackActive) \
|
||||
X(kXfbSpan, IsTransformFeedbackPaused) \
|
||||
X(kXfbSpan, GetTransformFeedbackGeneration) \
|
||||
X(kXfbSpan, GetBoundTransformFeedbackLifetimeId) \
|
||||
/* kProgramOp: ShaderStorageBlockBinding syncs Espryt's render state and textures */ \
|
||||
X(kProgramOp, GetRenderStateParameters) \
|
||||
X(kProgramOp, GetRenderStateParametersVersion) \
|
||||
X(kProgramOp, GetViewport) \
|
||||
X(kProgramOp, IsCapabilityEnabled) \
|
||||
X(kProgramOp, GetFramebufferBindingSlot) \
|
||||
X(kProgramOp, GetTextureUnitObject) \
|
||||
X(kProgramOp, GetTextureContextId) \
|
||||
X(kProgramOp, GetSamplingResolutionGeneration) \
|
||||
X(kProgramOp, GetTextureBindGeneration) \
|
||||
X(kProgramOp, GetMaxTouchedTextureUnit) \
|
||||
X(kProgramOp, GetImageTextureBinding) \
|
||||
/* kQuery: Magma's transform feedback query end reads the paused counter */ \
|
||||
/* (DirectVulkan.cpp); every other verb in the class reads nothing and */ \
|
||||
/* its fill is a serial bump */ \
|
||||
X(kQuery, GetTransformFeedbackPausedPrimitiveCounter)
|
||||
|
||||
// clang-format on
|
||||
@@ -0,0 +1,98 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipe.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>
|
||||
|
||||
#include "MGPipeCallbacks.h"
|
||||
#include "MGPipeHandles.h"
|
||||
#include "MGPipeHostSpan.h"
|
||||
#include "MGPipeTypes.h"
|
||||
|
||||
// The MGPipe boundary (plan B section 4).
|
||||
//
|
||||
// The two interface tables are FUNCTION-POINTER STRUCTS, not virtual bases. Three reasons
|
||||
// out of this repository rather than out of gallium: the boundary already is a
|
||||
// function-pointer struct sitting on one hook point in MG_Backend/Init.cpp; a nullptr entry
|
||||
// already means "not implemented, frontend falls back", which is exactly what a
|
||||
// not-yet-migrated subsystem needs to say while it keeps pulling; and MG_Test already
|
||||
// substitutes this table to mock a backend. The rare EGL and caps surface stays on
|
||||
// pActiveBackendObject's virtual functions.
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// Unscoped on purpose: PipeCalls.def spells these as bare tokens so the same file can
|
||||
// be read by the C++ preprocessor and by scripts/gen_pipe.py.
|
||||
enum MGPipeCallClass : Uint8 {
|
||||
kScreen,
|
||||
kCtxCso,
|
||||
kCtxState,
|
||||
kCtxObject,
|
||||
kCtxVerb,
|
||||
kCtxQuery,
|
||||
kCallClassCount,
|
||||
};
|
||||
|
||||
enum MGPipeCallFlags : Uint32 {
|
||||
kNone = 0,
|
||||
// The caller must not proceed until the server has acknowledged. Rare by design.
|
||||
kNeedsAck = 1u << 0,
|
||||
// Carries an MGPBlobRef.
|
||||
kHasBlob = 1u << 1,
|
||||
// Carries a variable-length array after the fixed payload.
|
||||
kVarTail = 1u << 2,
|
||||
// Carries an MGHostSpan - the one shape that changes with the transport.
|
||||
kHostSpan = 1u << 3,
|
||||
// Answers into an MGPReplySlot; never blocks.
|
||||
kReplySlot = 1u << 4,
|
||||
// May be null in a backend's table. A null entry is a real answer ("this backend
|
||||
// does not implement it"), not an error: DirectVulkan deliberately leaves
|
||||
// buffer_subdata_resident unregistered, and SetSwapInterval likewise.
|
||||
kOptional = 1u << 5,
|
||||
};
|
||||
|
||||
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
|
||||
// 4.5.2). Generated by G7 from the field list ComputePipelineStateHash already hashes;
|
||||
// MGPipeRenderStateSpans.cpp and the setter-consistency test land with P2, which is
|
||||
// when the chunk table can be filled with real offsets.
|
||||
struct MGPipeRenderStateSpans;
|
||||
|
||||
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
|
||||
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
||||
#include "PipeCalls.def"
|
||||
|
||||
// G1: the two interface tables. A null entry means "not implemented" (section 4.1).
|
||||
#include "generated/PipeTables.inc"
|
||||
|
||||
// The installed tables. Zero-initialized, so an un-installed MGPipe is every entry
|
||||
// null - which is precisely the pre-migration state.
|
||||
inline MGPipeScreen gMGPipeScreen{};
|
||||
inline MGPipeContext gMGPipeContext{};
|
||||
|
||||
// G2: monolith thunks. These are what MG_Impl call sites move onto, replacing
|
||||
// gBackendFunctionsTable.GL.* one name at a time.
|
||||
#include "generated/PipeThunks.inc"
|
||||
|
||||
// G3: wire records, their size assertions, and the applier's bounds precondition.
|
||||
#include "generated/PipeWire.inc"
|
||||
|
||||
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
|
||||
#include "generated/PipeVerify.inc"
|
||||
|
||||
// G5: PipeInputs field ids and the per-verb poison generations.
|
||||
#include "generated/PipeFilled.inc"
|
||||
|
||||
// G5b: the verb enum (one per GLFunctionsTable entry), the verb classes and their
|
||||
// may-read field masks - what MGPipeFillForVerb fills and what a poison build lets a
|
||||
// verb read (FillPoints.def).
|
||||
#include "generated/PipeFillPoints.inc"
|
||||
|
||||
// G6: the backend read inventory's coverage table.
|
||||
#include "generated/PipeCoverage.inc"
|
||||
|
||||
// G7: the render-state pipeline subset, by member name.
|
||||
#include "generated/PipeSpanTable.inc"
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,61 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeCallbacks.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>
|
||||
|
||||
#include "MGPipeHandles.h"
|
||||
#include "MGPipeTypes.h"
|
||||
|
||||
// The backend -> frontend reverse channel, named (plan B section 7.1).
|
||||
//
|
||||
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
|
||||
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification, texture
|
||||
// re-send requests or default-framebuffer geometry, because in Mesa the state tracker and
|
||||
// the driver share an address space. Naming them as ten callbacks plus one forward
|
||||
// terminator (MGPipeContext::ResourceSubDataComplete) is the deliberate deviation (D8).
|
||||
//
|
||||
// Installed at context creation. In a monolith these are direct calls; under split they are
|
||||
// records on the reverse channel, and their ORDER is a correctness requirement rather than
|
||||
// an optimization (section 7.4).
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct MGPipeCallbacks {
|
||||
// A driver-detected GL error that only the server could have seen.
|
||||
void (*OnGlError)(Uint32 code);
|
||||
// Ranges of a resource the GPU wrote; retires MarkGpuWritten.
|
||||
void (*OnGpuWritten)(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges);
|
||||
void (*OnBufferWriteback)(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes);
|
||||
void (*OnTextureWriteback)(MGPipeHandle res, const MGPBox* box, MGPBlobRef bytes);
|
||||
// The one new stall class in this design (D-B6): the server recast a texture and
|
||||
// needs its texels back. The client answers with zero or more ResourceSubData
|
||||
// records terminated by ResourceSubDataComplete carrying the same pullSerial.
|
||||
void (*OnTexturePullRequest)(MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount,
|
||||
Uint64 pullSerial);
|
||||
// SHAPE ONLY, never bytes: the client owns the CPU shadow and allocates the levels
|
||||
// itself.
|
||||
void (*OnMipLevelsGenerated)(MGPipeHandle res, Uint16 base, Uint16 count);
|
||||
// Retires the layering inversion where the swapchain writes into MG_Impl's
|
||||
// pDefaultFramebufferInfo.
|
||||
void (*OnSurfaceChanged)(const MGPSurfaceInfo* info);
|
||||
void (*OnCapsInvalidated)();
|
||||
// <= WARN is lossy, >= ERROR is lossless and rate limited.
|
||||
void (*OnLog)(Uint8 level, const char* text);
|
||||
// The XFB scatter is a read-modify-write of the CLIENT's shadow, so the server
|
||||
// hands back the packed scratch and the client scatters (section 7.2.1).
|
||||
void (*OnXfbScatterReady)(MGPipeHandle scratch, Uint64 packedStride, Uint64 vertices);
|
||||
};
|
||||
|
||||
// Ten, and the count is asserted so an eleventh cannot be added without touching the
|
||||
// transport's reverse-channel record table.
|
||||
inline constexpr SizeT kMGPipeCallbackCount = 10;
|
||||
static_assert(sizeof(MGPipeCallbacks) == kMGPipeCallbackCount * sizeof(void (*)()),
|
||||
"MGPipeCallbacks gained or lost a callback");
|
||||
|
||||
// Null-initialized: a backend that installs nothing sends nothing.
|
||||
inline MGPipeCallbacks gMGPipeCallbacks{};
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,98 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeHandles.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>
|
||||
|
||||
// MGPipe object identity (plan B section 4.2).
|
||||
//
|
||||
// A handle is a {slot, gen} pair minted by the CLIENT and never by the server: no create_*
|
||||
// call in the catalogue returns a server-cast handle, which is the deliberate deviation
|
||||
// from gallium (D1) that lets the whole catalogue be remoted with ZERO creation round
|
||||
// trips.
|
||||
//
|
||||
// Slots are dense and allocated PER KIND, so the server's object table is an array rather
|
||||
// than a hash map. The allocator is a free list plus a high-water mark and has nothing to
|
||||
// do with MG_State's IndexGenerator - that container's LIFO name reuse is the very problem
|
||||
// {slot, gen} exists to close.
|
||||
namespace MobileGL::MG_Pipe {
|
||||
enum class MGPipeKind : Uint8 {
|
||||
None = 0,
|
||||
Buffer = 1,
|
||||
Texture,
|
||||
Renderbuffer,
|
||||
Framebuffer,
|
||||
Xfb,
|
||||
RenderStateCso,
|
||||
VertexElementsCso,
|
||||
SamplerCso,
|
||||
SamplerViewCso,
|
||||
ShaderCso,
|
||||
Fence,
|
||||
Query,
|
||||
Context,
|
||||
KindCount,
|
||||
};
|
||||
|
||||
// 8 bytes, POD, passed by value in a register pair.
|
||||
//
|
||||
// Gen increments only when a SLOT IS REUSED - never on a respecify - so {slot, gen} is
|
||||
// unique until the same slot has been recycled 2^32 times. That bound is documented
|
||||
// rather than defended at runtime in release builds: at one recycle per frame at
|
||||
// 1000 fps a single slot would take ~50 days of continuous churn to wrap, and the
|
||||
// debug allocator asserts on the wrap.
|
||||
//
|
||||
// Two generations exist in this design and they are strictly separate (section 4.2.2):
|
||||
// this one is the CLIENT's answer to "is this still the same GL object", while MGGen is
|
||||
// the SERVER's own epoch for "did I recast my driver object". Interface rule: no MGPipe
|
||||
// call may require the client to supply or know MGGen.
|
||||
struct MGPipeHandle {
|
||||
Uint32 Slot;
|
||||
Uint32 Gen;
|
||||
|
||||
friend constexpr Bool operator==(const MGPipeHandle& a, const MGPipeHandle& b) {
|
||||
return a.Slot == b.Slot && a.Gen == b.Gen;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(MGPipeHandle) == 8, "MGPipeHandle is the 8-byte {slot, gen} pair");
|
||||
static_assert(alignof(MGPipeHandle) == 4, "MGPipeHandle must not gain padding on the wire");
|
||||
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
|
||||
|
||||
// Reserved handles (section 4.2.1).
|
||||
// {0, 0} is null for every kind.
|
||||
// {0, 1} of kind Framebuffer is the DEFAULT framebuffer. It exists so the four
|
||||
// pDefaultFramebufferInfo->defaultFBO identity comparisons in DirectGLES retire into
|
||||
// an ordinary handle compare.
|
||||
inline constexpr MGPipeHandle kMGPipeNullHandle{0, 0};
|
||||
inline constexpr MGPipeHandle kMGPipeDefaultFramebuffer{0, 1};
|
||||
|
||||
inline constexpr Bool MGPipeHandleIsNull(const MGPipeHandle& handle) {
|
||||
return handle.Slot == 0 && handle.Gen == 0;
|
||||
}
|
||||
|
||||
// Slot 0 of every kind is reserved (null, and the default framebuffer for kind
|
||||
// Framebuffer), so a real allocation starts at 1.
|
||||
inline constexpr Uint32 kMGPipeFirstAllocatableSlot = 1;
|
||||
|
||||
// ShaderCso slot space. The top 1/16 of it is reserved for PROGRAM PIPELINE COMPOSITES
|
||||
// (section 5.6.3): a composite is minted client-side out of the stage programs bound to
|
||||
// a pipeline object, and the server never learns it is a composite - it is just another
|
||||
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
||||
// lifetime bookkeeping out of the ordinary program slot allocator.
|
||||
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
||||
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
||||
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
||||
|
||||
inline constexpr Bool MGPipeIsCompositeShaderSlot(Uint32 slot) {
|
||||
return slot >= kMGPipeShaderCsoCompositeSlotBase && slot < kMGPipeShaderCsoSlotLimit;
|
||||
}
|
||||
|
||||
static_assert(kMGPipeShaderCsoCompositeSlotBase > kMGPipeFirstAllocatableSlot,
|
||||
"the composite band must not swallow the ordinary program slots");
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,57 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeHostSpan.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>
|
||||
|
||||
// The ONE thing in MGPipe whose shape changes with the transport (plan B section 4.5.7).
|
||||
//
|
||||
// Monolith: Ptr addresses the frontend shadow or the application's own memory and the
|
||||
// accessor is one predictable branch. Split: Ptr is null and the bytes live in a staging
|
||||
// segment named by Seg/Offset, or - for the index bytes a server-side primitive-restart
|
||||
// rewrite or multi-draw flattening consumes - in the server's own index host mirror, which
|
||||
// costs no wire traffic at all (D-B7).
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// Seg sentinels. Anything else is a real SEG_STAGE id assigned by the transport.
|
||||
inline constexpr Uint32 kMGHostSpanSegNone = 0;
|
||||
// "The bytes are already on your side": the server reads them out of the index host
|
||||
// mirror it maintains for every resource created with the ELEMENT_ARRAY bind bit while
|
||||
// kCapNeedsHostIndexBytes is set. When the mirror is over budget the tracker degrades
|
||||
// to per-draw staging and counts the bytes in index-bytes-shipped.
|
||||
inline constexpr Uint32 kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFFu;
|
||||
|
||||
struct MGHostSpan {
|
||||
// Field order is chosen so the struct is 32 bytes with natural alignment on both a
|
||||
// 64-bit and a 32-bit host: the pointer and the two 32-bit words fill the first
|
||||
// 16-byte block either way.
|
||||
const void* Ptr;
|
||||
Uint32 Seg;
|
||||
Uint32 Pad0;
|
||||
Uint64 Size;
|
||||
Uint64 Offset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(MGHostSpan) == 32, "MGHostSpan is the 32-byte host-bytes descriptor");
|
||||
static_assert(std::is_trivially_copyable_v<MGHostSpan>);
|
||||
|
||||
// Split-mode resolution needs the transport's segment table, which does not exist in a
|
||||
// monolith build; the hook is a weak-ish indirection installed by MG_Remote when it is
|
||||
// compiled in. In P0 there is no transport, so a span that names a segment resolves to
|
||||
// null and every caller is still on the monolith branch.
|
||||
using MGPipeSegmentResolver = const void* (*)(Uint32 seg, Uint64 offset, Uint64 size);
|
||||
inline MGPipeSegmentResolver gMGPipeSegmentResolver = nullptr;
|
||||
|
||||
// One predictable branch on the hot path.
|
||||
inline const void* MGPipeHostBytes(const MGHostSpan& span) {
|
||||
if (span.Ptr != nullptr) {
|
||||
return static_cast<const Uint8*>(span.Ptr) + span.Offset;
|
||||
}
|
||||
if (gMGPipeSegmentResolver == nullptr) return nullptr;
|
||||
return gMGPipeSegmentResolver(span.Seg, span.Offset, span.Size);
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,807 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeTypes.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>
|
||||
|
||||
#include "MGPipeHandles.h"
|
||||
#include "MGPipeHostSpan.h"
|
||||
|
||||
// Every MGPipe payload (plan B section 4.5). Each one is a flat POD with explicit padding,
|
||||
// carries a static_assert on trivial copyability and one on its exact size, and never
|
||||
// contains a pointer: MGHostSpan, the one shape that changes with the transport, only ever
|
||||
// rides in a variable tail (draw_vbo's user indices, set_shader_buffers' named-UBO bytes),
|
||||
// never inline in a fixed payload.
|
||||
//
|
||||
// Sizes are asserted rather than merely documented because the wire records generated from
|
||||
// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a
|
||||
// protocol break that no test would otherwise see.
|
||||
//
|
||||
// P0.5 DEBT, half repaid. The MG_State half is gone: ResidualValueBlock's
|
||||
// RenderStateParameters and PixelStoreParameters now come from MGPipeValueTypes.h, so
|
||||
// this header no longer reaches RenderState.h (its closure still touches TextureEnum.h,
|
||||
// through BackendObject.h, for the reason in the next sentence). What remains is MGPCaps embedding
|
||||
// MG_Backend's DynamicBackendParameters - deliberate, the caps block IS that struct
|
||||
// (section 4.4.1) - and that one include is what still keeps purity gate A (section
|
||||
// 10.3) off this header; the gate asserts MGPipeValueTypes.h instead. The caps block
|
||||
// needs fixed-width members before it can move (a type change, not a move): P1/P7.
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include "MGPipeValueTypes.h"
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
using MG_Backend::DynamicBackendParameters;
|
||||
// Both live directly in namespace MobileGL and, since P0.5, are declared in
|
||||
// MG_Pipe/MGPipeValueTypes.h.
|
||||
using MobileGL::PixelStoreParameters;
|
||||
using MobileGL::RenderStateParameters;
|
||||
|
||||
// A payload must be memcpy-able and its size must be an exact, stated number.
|
||||
#define MGP_ASSERT_POD(T, Size) \
|
||||
static_assert(std::is_trivially_copyable_v<T>, #T " must be trivially copyable"); \
|
||||
static_assert(sizeof(T) == (Size), #T " changed size; update the wire format and this assertion")
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Shared primitives
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// A run of bytes in the command stream's blob area. Monolith: Seg is
|
||||
// kMGHostSpanSegNone and Offset is an address into the caller's staging arena. Split:
|
||||
// Seg names a transport segment.
|
||||
struct MGPBlobRef {
|
||||
Uint64 Offset;
|
||||
Uint64 Size;
|
||||
Uint32 Seg;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBlobRef, 24);
|
||||
|
||||
struct MGPRange {
|
||||
Uint64 Offset;
|
||||
Uint64 Size;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPRange, 16);
|
||||
|
||||
// Destination box in the level's own coordinate system (section 4.5.6).
|
||||
struct MGPBox {
|
||||
Int32 X, Y, Z;
|
||||
Uint32 W, H, D;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBox, 24);
|
||||
|
||||
// Where an asynchronous answer lands. Every server query in this catalogue is
|
||||
// async-with-handle; none of them blocks (section 4.4.6, "the total rule").
|
||||
struct MGPReplySlot {
|
||||
Uint64 Id;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPReplySlot, 8);
|
||||
|
||||
// One contiguous run of RenderStateParameters bytes. The pipeline/dynamic split is
|
||||
// defined exactly once, in MGPipeRenderStateSpans, and generated by G7 from the field
|
||||
// list VulkanRenderer::ComputePipelineStateHash already hashes (section 4.5.2).
|
||||
struct MGPStateChunk {
|
||||
Uint16 Offset;
|
||||
Uint16 Length;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPStateChunk, 4);
|
||||
|
||||
// The payload of every call that carries nothing but an object identity.
|
||||
struct MGPHandleOnly {
|
||||
MGPipeHandle Handle;
|
||||
Uint32 Kind; // MGPipeKind, widened for a stable wire size
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPHandleOnly, 16);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Screen: caps, resources, fences
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Capability bits that replace "is this table slot null" as an implicit feature probe
|
||||
// (section 4.4.1). The five ownership-switch bits of v1 are deliberately absent: what
|
||||
// they tried to express - who performs primitive-restart rewriting and multi-draw
|
||||
// flattening - is not expressible as a capability (D-B7).
|
||||
enum MGPCapBit : Uint64 {
|
||||
kCapNone = 0,
|
||||
kCapViewportArray = 1ull << 0,
|
||||
kCapFloat64VertexAttrib = 1ull << 1,
|
||||
kCapResidentSubData = 1ull << 2,
|
||||
kCapCpuXfbPrimitiveAccounting = 1ull << 3,
|
||||
kCapTimerQuery = 1ull << 4,
|
||||
kCapOcclusionQuery = 1ull << 5,
|
||||
kCapXfbPrimitivesQuery = 1ull << 6,
|
||||
// The server rewrites restart indices / flattens multi-draws itself and therefore
|
||||
// needs the index bytes on its side: under split this arms the index host mirror
|
||||
// (D-B7).
|
||||
kCapNeedsHostIndexBytes = 1ull << 7,
|
||||
// The server packs named uniform blocks into its own ring and therefore needs the
|
||||
// host bytes of a set_shader_buffers(Uniform) range (D-B8).
|
||||
kCapNeedsHostUboBytes = 1ull << 8,
|
||||
};
|
||||
|
||||
struct MGPCaps {
|
||||
// The ~90 flat scalars the backends already publish, by inclusion rather than by
|
||||
// restatement: a caps field added there must not need a second edit here. This is
|
||||
// also where the six per-axis compute limits (MaxComputeWorkGroupCount/Size) ride -
|
||||
// the only indexed answers the device owns, and therefore the only ones that outlive
|
||||
// the GetIntegeri_v table entry (see the PipeCalls.def footer).
|
||||
DynamicBackendParameters Dynamic;
|
||||
Uint64 CallMask; // MGPCapBit
|
||||
// The two halves that are not flat PODs travel as blobs: the format capability
|
||||
// cache holds Vector<Int> sample-count lists, and the renderer strings are
|
||||
// Strings. Their serializers land with the transport (P5).
|
||||
MGPBlobRef FormatCapabilities;
|
||||
MGPBlobRef RendererInfo;
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<MGPCaps>, "MGPCaps must be trivially copyable");
|
||||
// Stated as a COMPOSITION rather than a literal: DynamicBackendParameters still carries
|
||||
// SizeT fields, so its literal size is ABI-dependent until P0.5 moves the caps block
|
||||
// into MGPipeValueTypes.h with fixed-width members. The assertion still fires on any
|
||||
// padding introduced between the members below.
|
||||
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
||||
"MGPCaps gained padding or a member; update the wire format");
|
||||
|
||||
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
|
||||
// share one create/respecify shape (section 4.5.1).
|
||||
struct MGPResourceDesc {
|
||||
MGPipeHandle Resource;
|
||||
Uint8 Target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
|
||||
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
|
||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
|
||||
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
|
||||
Uint16 BindMask;
|
||||
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
||||
Uint32 Width, Height, Depth;
|
||||
Uint16 ArrayLayers, Levels, Samples;
|
||||
Uint8 FixedSampleLocations, Immutable;
|
||||
Uint32 Usage; // BufferUsage
|
||||
Uint32 StorageFlags; // glBufferStorage flags
|
||||
Uint8 HasDefinedContent; // false after a NULL-data respecify
|
||||
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
|
||||
Uint16 Pad0;
|
||||
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
|
||||
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
|
||||
// cannot hold one.
|
||||
Uint32 GlNameForDiag;
|
||||
Uint32 Pad1;
|
||||
MGPipeHandle ViewOf; // storage owner for a texture view
|
||||
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
|
||||
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
|
||||
};
|
||||
MGP_ASSERT_POD(MGPResourceDesc, 88);
|
||||
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
|
||||
|
||||
struct MGPFenceWait {
|
||||
MGPipeHandle Fence;
|
||||
Uint64 TimeoutNs;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPFenceWait, 16);
|
||||
|
||||
struct MGPQueryDesc {
|
||||
MGPipeHandle Query;
|
||||
Uint32 Kind; // GL query target
|
||||
Uint32 Stream; // indexed query stream, 0 otherwise
|
||||
};
|
||||
MGP_ASSERT_POD(MGPQueryDesc, 16);
|
||||
|
||||
struct MGPQueryResultRequest {
|
||||
MGPipeHandle Query;
|
||||
Uint8 Wait; // the two-value contract of GetSyncStatus is preserved verbatim
|
||||
Uint8 Pad0[3];
|
||||
Uint32 Pad1;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPQueryResultRequest, 16);
|
||||
|
||||
// query_timestamp: glGetInteger64v(GL_TIMESTAMP), the synchronous "what time is it on the
|
||||
// GPU" GLFunctionsTable::GetGpuTimestampNs answers today. The request names nothing; the
|
||||
// Int64 nanosecond stamp comes back through the reply slot.
|
||||
struct MGPTimestampRequest {
|
||||
Uint32 Reserved;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPTimestampRequest, 8);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// CSOs
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// create_render_state carries ONLY the pipeline subset's chunk bytes. chunkMask lets an
|
||||
// incremental create send just the chunks that moved, against baseCso (section 4.5.2).
|
||||
struct MGPRenderStateDesc {
|
||||
MGPipeHandle Cso;
|
||||
MGPipeHandle BaseCso;
|
||||
Uint32 ChunkMask; // all ones for a brand new CSO
|
||||
Uint32 Pad0;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPRenderStateDesc, 48);
|
||||
|
||||
// Steady state: 12 bytes on the wire, no hashing, no blob.
|
||||
struct MGPBindRenderState {
|
||||
MGPipeHandle Cso;
|
||||
Uint16 Version;
|
||||
Uint16 PipelineVersion;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBindRenderState, 12);
|
||||
|
||||
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
|
||||
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
|
||||
// values, sample coverage, hints and the point-size family. This is what keeps
|
||||
// glViewport from evicting Magma's pipeline memo (D-B1).
|
||||
struct MGPDynamicState {
|
||||
Uint32 ChunkMask;
|
||||
Uint16 Version;
|
||||
Uint16 Pad0;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPDynamicState, 32);
|
||||
|
||||
// Both views travel, and neither is derivable from the other: the resolved
|
||||
// VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means
|
||||
// "element size" while a binding-model stride of 0 means "every vertex reads the same
|
||||
// element" (section 4.5.3). IsLong and Type == Float64 are carried separately.
|
||||
struct MGPVertexElements {
|
||||
MGPipeHandle Cso;
|
||||
Uint32 AttributeCount;
|
||||
Uint32 BindingPointCount;
|
||||
MGPBlobRef Blob; // VertexAttribute[] followed by VertexBufferBindingPoint[]
|
||||
};
|
||||
MGP_ASSERT_POD(MGPVertexElements, 40);
|
||||
|
||||
// SamplerParameters crosses byte for byte INCLUDING borderColorForm: without it the
|
||||
// backend cannot choose between glSamplerParameterIiv and fv, or between the
|
||||
// VkBorderColor families, because all three representations are always numerically
|
||||
// populated (section 4.5.4). Carried as a blob until P0.5 gives it a value header.
|
||||
struct MGPSamplerDesc {
|
||||
MGPipeHandle Cso;
|
||||
MGPBlobRef Parameters;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSamplerDesc, 32);
|
||||
|
||||
// = pipe_sampler_view, and ONLY the view restrictions. Everything a glTexParameter
|
||||
// writes lives on set_texture_params instead, because a texture that is only an FBO
|
||||
// attachment, only an image binding or only a glCopyImageSubData endpoint has no
|
||||
// sampler view to hang it on (section 4.4.3).
|
||||
struct MGPSamplerView {
|
||||
MGPipeHandle Cso;
|
||||
MGPipeHandle Texture;
|
||||
Uint32 InternalFormat; // aliasing format for glTextureView
|
||||
Uint8 Target;
|
||||
Uint8 Pad0[3];
|
||||
Uint16 MinLevel, NumLevels, MinLayer, NumLayers;
|
||||
Uint16 Samples;
|
||||
Uint8 FixedSampleLocations;
|
||||
Uint8 Pad1;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSamplerView, 36);
|
||||
|
||||
// Per texture OBJECT, independent of any view.
|
||||
struct MGPTextureParams {
|
||||
MGPipeHandle Res;
|
||||
Uint16 BaseLevel, MaxLevel;
|
||||
Uint8 Swizzle[4];
|
||||
Uint8 DepthStencilMode;
|
||||
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
||||
// override that the frontend params version does not move for.
|
||||
Uint8 ForceResync;
|
||||
Uint8 Pad0[2];
|
||||
Float MinLod, MaxLod, LodBias;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPTextureParams, 32);
|
||||
|
||||
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
|
||||
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
|
||||
// deserialize into them without dragging in glslang (section 4.5.5).
|
||||
struct MGPProgramDesc {
|
||||
MGPipeHandle Cso;
|
||||
Uint32 StageMask; // == GetLinkedShaderStages()
|
||||
Uint32 GlobalUboSize;
|
||||
Uint32 ReservedNumSamplesOffset;
|
||||
Uint8 SpirvStatus;
|
||||
Uint8 NativeFloat64;
|
||||
Uint8 PointSizeDemoted;
|
||||
Uint8 EnableSpirvValidation;
|
||||
MGPBlobRef Spirv[6]; // per stage
|
||||
MGPBlobRef Reflection;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPProgramDesc, 192);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// set_*
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
|
||||
// push time with no lookup (section 4.5.6).
|
||||
struct MGPSurface {
|
||||
MGPipeHandle Res;
|
||||
Uint32 InternalFormat;
|
||||
Uint8 Kind; // Texture | Renderbuffer | None
|
||||
Uint8 Layered;
|
||||
Uint16 Level;
|
||||
Uint32 Layer;
|
||||
Uint16 UploadTarget;
|
||||
Uint16 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSurface, 24);
|
||||
|
||||
struct MGPFramebufferState {
|
||||
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
||||
MGPSurface Color[8];
|
||||
MGPSurface Depth, Stencil;
|
||||
// The RESOLVED read surface, not an index. This is what structurally closes the
|
||||
// read-buffer-shared-FBO defect class.
|
||||
MGPSurface ReadSurface;
|
||||
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
|
||||
Uint16 Width, Height, Layers, Samples;
|
||||
Uint8 FixedSampleLocations, IsDefault, Complete, Pad0;
|
||||
Uint32 Pad1;
|
||||
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
|
||||
// emission suppressor - an unchanged hash means this record is not sent at all.
|
||||
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
|
||||
// redundant glBindSampler traffic reappears as a variable-length record per batch.
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPFramebufferState, 304);
|
||||
|
||||
struct MGPVertexBuffer {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset;
|
||||
Uint32 Stride;
|
||||
Uint32 Divisor;
|
||||
Uint32 BindingIndex;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPVertexBuffer, 32);
|
||||
|
||||
// Var-tail header: MGPVertexBuffer[Count] follows.
|
||||
struct MGPVertexBuffers {
|
||||
Uint32 Start, Count;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPVertexBuffers, 16);
|
||||
|
||||
// An independent call, NOT a subset of the VAO configuration version (D5).
|
||||
struct MGPIndexBuffer {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset;
|
||||
Uint32 IndexSize;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPIndexBuffer, 24);
|
||||
|
||||
struct MGPIndirectBuffers {
|
||||
MGPipeHandle DrawIndirect;
|
||||
MGPipeHandle Parameter;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPIndirectBuffers, 16);
|
||||
|
||||
// One entry of set_sampler_views. No stage dimension: MobileGL's texture unit space is
|
||||
// MERGED (TextureState::m_textureUnits is one Array of MAX_TEXTURE_IMAGE_UNITS = 192),
|
||||
// and the same unit may be sampled from two stages (section 4.4.3).
|
||||
struct MGPBoundView {
|
||||
MGPipeHandle View;
|
||||
MGPipeHandle Texture;
|
||||
Uint32 Unit;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBoundView, 24);
|
||||
|
||||
struct MGPSamplerViews {
|
||||
Uint32 Start, Count;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSamplerViews, 16);
|
||||
|
||||
// Var-tail header: MGPipeHandle[Count] of sampler CSOs follows.
|
||||
struct MGPSamplerStates {
|
||||
Uint32 Start, Count;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSamplerStates, 16);
|
||||
|
||||
struct MGPImageView {
|
||||
MGPipeHandle Res;
|
||||
Uint32 Unit;
|
||||
Uint32 InternalFormat;
|
||||
Uint32 Layer;
|
||||
Uint16 Level;
|
||||
Uint8 Layered;
|
||||
Uint8 Access;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPImageView, 24);
|
||||
|
||||
struct MGPShaderImages {
|
||||
Uint32 Start, Count;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPShaderImages, 16);
|
||||
|
||||
// One bound buffer range: 24 bytes, no inline host span. The named-UBO host bytes a
|
||||
// backend needs under kCapNeedsHostUboBytes (D-B8) travel as an OPTIONAL second var-tail,
|
||||
// MGHostSpan[HostSpanCount] behind the ranges, announced by MGPShaderBuffers below. An
|
||||
// inline span would have cost every SSBO, atomic-counter and XFB range 32 dead bytes, and
|
||||
// D-B8 says not to freeze that payload's shape before the stage-ubo-named counter has
|
||||
// produced numbers.
|
||||
struct MGPBufferRange {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset;
|
||||
Uint64 Size;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBufferRange, 24);
|
||||
|
||||
// Var-tail header: MGPBufferRange[Count], then MGHostSpan[HostSpanCount]. HostSpanCount is
|
||||
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (a range with nothing to
|
||||
// ship carries an empty span, so the two arrays stay index-aligned).
|
||||
struct MGPShaderBuffers {
|
||||
Uint32 Class; // Uniform | ShaderStorage | AtomicCounter
|
||||
Uint32 Start;
|
||||
Uint32 Count;
|
||||
Uint32 WritableMask;
|
||||
Uint32 HostSpanCount; // 0, or Count when the kHostSpan tail is present (D-B8)
|
||||
Uint32 Pad0;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPShaderBuffers, 32);
|
||||
|
||||
// Var-tail header: MGPBufferRange[Count] then Uint32 offsets[Count].
|
||||
struct MGPStreamOutputTargets {
|
||||
Uint32 Count;
|
||||
Uint32 Pad0;
|
||||
Uint64 Generation;
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPStreamOutputTargets, 24);
|
||||
|
||||
// Covers the DEFAULT UNIFORM BLOCK only (D6).
|
||||
struct MGPGlobalConstants {
|
||||
MGPipeHandle ShaderCso;
|
||||
Uint32 Version;
|
||||
Uint32 Pad0;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPGlobalConstants, 40);
|
||||
|
||||
// The float/int/uint view is resolved on the CLIENT by ClassifyVertexAttribType.
|
||||
struct MGPAttribValue {
|
||||
Uint32 Location;
|
||||
Uint8 ValueClass; // Float | Int | Uint | Double
|
||||
Uint8 Pad0[3];
|
||||
Uint32 Data[4];
|
||||
};
|
||||
MGP_ASSERT_POD(MGPAttribValue, 24);
|
||||
|
||||
// Var-tail header: MGPAttribValue[popcount(Mask)] follows.
|
||||
struct MGPVertexAttribDefaults {
|
||||
Uint32 Mask;
|
||||
Uint32 Count;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPVertexAttribDefaults, 8);
|
||||
|
||||
// PACK only. There is deliberately no unpack counterpart: nothing on the far side of
|
||||
// the boundary reads unpack state (section 4.6 D5), and the staged-repack upload path
|
||||
// does not even issue glPixelStorei.
|
||||
struct MGPPixelPackState {
|
||||
PixelStoreParameters Pack;
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<MGPPixelPackState>);
|
||||
// 28 is what PixelStoreParameters measures: two Bools, two bytes of padding, six Ints.
|
||||
// Asserting against sizeof(PixelStoreParameters) itself was a tautology that could not
|
||||
// notice the value struct changing width under the wire format.
|
||||
static_assert(sizeof(MGPPixelPackState) == 28,
|
||||
"MGPPixelPackState changed size; update the wire format and this assertion");
|
||||
|
||||
// Also a shader-variant input: both backends bake these into the synthesized
|
||||
// pass-through control stage.
|
||||
struct MGPPatchState {
|
||||
Uint32 Vertices;
|
||||
Uint32 Pad0;
|
||||
Float Outer[4];
|
||||
Float Inner[2];
|
||||
Uint32 Pad1[2];
|
||||
};
|
||||
MGP_ASSERT_POD(MGPPatchState, 40);
|
||||
|
||||
// Migration-only (section 6.3). Every stage removes fields and lowers
|
||||
// MGL_RESIDUAL_BLOCK_SIZE; P13 asserts it is zero, which is the retirement trip wire.
|
||||
//
|
||||
// Layout must be asserted MEMBER BY MEMBER, not only by sizeof: a heterogeneous POD
|
||||
// union is where padding differs across ABIs, and the monolith verify harness is blind
|
||||
// to it because both sides are the same translation unit. G3 emits the offsetof
|
||||
// assertions; under split the block is serialized field-wise rather than memcpy'd.
|
||||
struct ResidualValueBlock {
|
||||
RenderStateParameters RenderState; // until create/bind_render_state + set_dynamic_state land
|
||||
PixelStoreParameters Pack; // until set_pixel_pack_state lands
|
||||
Uint64 CapabilityBits;
|
||||
Uint32 PatchVertices;
|
||||
Uint32 Pad0;
|
||||
Float PatchOuter[4];
|
||||
Float PatchInner[2];
|
||||
Uint32 Pad1[2];
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
|
||||
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
|
||||
// set_* call deletes fields here and lowers it, and P13 replaces it with
|
||||
// static_assert(sizeof(ResidualValueBlock) == 0), which stays red until the last field is
|
||||
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
|
||||
// break - which is the point.
|
||||
//
|
||||
// Stable across the ABIs MobileGL ships on: every member of RenderStateParameters and
|
||||
// PixelStoreParameters is a fixed-width scalar or an array of one, with no pointer and no
|
||||
// SizeT.
|
||||
#define MGL_RESIDUAL_BLOCK_SIZE 1248
|
||||
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
|
||||
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
|
||||
"retired, and do not raise it");
|
||||
|
||||
struct MGPResidualValueState {
|
||||
Uint32 Version;
|
||||
Uint32 Pad0;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPResidualValueState, 32);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Transfer
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Shape copied from the unpack ring's existing UnpackStagingBlock. The source strides
|
||||
// are CARRIED, not inferred from a pointer comparison: the old
|
||||
// `uploadData == mipData` test cannot survive a split, where the client neither ships
|
||||
// the whole level nor keeps a server-side mirror of it (section 4.5.6).
|
||||
struct MGPSubRegion {
|
||||
Int32 X, Y, Z;
|
||||
Uint32 W, H, D;
|
||||
Uint64 SrcOffset; // into the blob
|
||||
Uint32 SrcRowStride; // bytes; 0 = tightly packed (w * bpp)
|
||||
Uint32 SrcSliceStride; // bytes; 0 = tightly packed
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSubRegion, 40);
|
||||
|
||||
// Carries the union box AND the region list so the SERVER picks the upload shape - the
|
||||
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
|
||||
// JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame.
|
||||
//
|
||||
// THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination
|
||||
// byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte
|
||||
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0, and
|
||||
// Blob holds exactly Size source bytes. That caps ONE record at a 2^31-1 offset and a
|
||||
// 2^32-1 size; a range beyond either is split by the emitter - the same rule, and at
|
||||
// SEG_STAGE's 32 MiB the far tighter one, that the ring's half-capacity bound already
|
||||
// imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are
|
||||
// the only spelling of this convention; nothing else reads the box for a buffer.
|
||||
struct MGPSubData {
|
||||
MGPipeHandle Res;
|
||||
Uint16 Target, Level;
|
||||
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
|
||||
// bytes an untransformed level shadow?
|
||||
Uint8 SourceIsVerbatimLevelShadow;
|
||||
Uint8 Pad0[3];
|
||||
MGPBox UnionBox;
|
||||
Uint32 RegionCount; // MGPSubRegion[] in the variable tail
|
||||
Uint32 Pad1;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSubData, 72);
|
||||
|
||||
// Encodes a buffer byte range into the record's box. False, with the record untouched,
|
||||
// when the range does not fit one record: the emitter has to split it.
|
||||
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
|
||||
if (offset > 0x7FFFFFFFull || size > 0xFFFFFFFFull) {
|
||||
return false;
|
||||
}
|
||||
record.UnionBox = MGPBox{static_cast<Int32>(offset), 0, 0, static_cast<Uint32>(size), 1, 1};
|
||||
record.Level = 0;
|
||||
record.RegionCount = 0;
|
||||
return true;
|
||||
}
|
||||
inline Uint64 MGPipeSubDataBufferOffset(const MGPSubData& record) {
|
||||
// A negative X is a corrupt record (the encoder never writes one); read as unsigned
|
||||
// it lands above the encodable bound, which the applier's bounds gate refuses.
|
||||
return static_cast<Uint64>(static_cast<Uint32>(record.UnionBox.X));
|
||||
}
|
||||
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
|
||||
|
||||
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
|
||||
// zero regions - that is how a pull that needs nothing is answered.
|
||||
struct MGPSubDataComplete {
|
||||
MGPipeHandle Res;
|
||||
Uint16 Target, FirstLevel, LevelCount, Pad0;
|
||||
Uint64 PullSerial;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSubDataComplete, 24);
|
||||
|
||||
// Carries the application's REAL access flags, not a normalized subset.
|
||||
struct MGPFlushRange {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset, Size;
|
||||
Uint32 AccessFlags; // Flags<BufferMappingAccessBit>
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPFlushRange, 32);
|
||||
|
||||
struct MGPReadback {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset, Size;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPReadback, 24);
|
||||
|
||||
struct MGPCopyRegion {
|
||||
MGPipeHandle Src, Dst;
|
||||
MGPBox SrcBox;
|
||||
Int32 DstX, DstY, DstZ;
|
||||
Uint16 SrcTarget, DstTarget;
|
||||
Uint16 SrcLevel, DstLevel;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPCopyRegion, 64);
|
||||
|
||||
struct MGPBlit {
|
||||
MGPipeHandle ReadFbo, DrawFbo;
|
||||
Int32 SrcX0, SrcY0, SrcX1, SrcY1;
|
||||
Int32 DstX0, DstY0, DstX1, DstY1;
|
||||
Uint32 Mask;
|
||||
Uint32 Filter;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPBlit, 56);
|
||||
|
||||
// One discriminated record replacing glClear, the four glClearBuffer* and the four
|
||||
// glClearNamedFramebuffer* entry points (section 4.4.4).
|
||||
struct MGPClear {
|
||||
MGPipeHandle Fbo;
|
||||
Uint32 Kind; // Whole | Color | Depth | Stencil | DepthStencil
|
||||
Int32 DrawBufferIndex;
|
||||
Uint32 BufferMask; // GL_COLOR_BUFFER_BIT etc. for the whole-framebuffer form
|
||||
Uint32 ValueClass; // Float | Int | Uint
|
||||
Uint32 ColorValue[4];
|
||||
Float DepthValue;
|
||||
Int32 StencilValue;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPClear, 48);
|
||||
|
||||
struct MGPMipPlan {
|
||||
MGPipeHandle Res;
|
||||
Uint16 Target, BaseLevel, LevelCount, Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPMipPlan, 16);
|
||||
|
||||
// read_pixels and get_texture_image share one shape; both answer into a reply slot.
|
||||
struct MGPReadbackInfo {
|
||||
MGPipeHandle Res; // null for read_pixels: the bound read surface answers
|
||||
MGPBox Box;
|
||||
Uint32 Format, Type;
|
||||
Uint16 Target, Level;
|
||||
Uint32 Pad0;
|
||||
Uint64 DstOffset, DstSize;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPReadbackInfo, 64);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
enum MGPDrawFlagBit : Uint8 {
|
||||
kDrawHasUserIndices = 1u << 0,
|
||||
kDrawPrimitiveRestart = 1u << 1,
|
||||
kDrawIndicesAreClient = 1u << 2,
|
||||
kDrawHasIndexRange = 1u << 3,
|
||||
kDrawHasXfbCount = 1u << 4,
|
||||
};
|
||||
|
||||
// = pipe_draw_info. Today's twenty draw entry points collapse onto this one call, with
|
||||
// MGPDrawRange[] holding exactly the shape the glMultiDraw* family already has.
|
||||
//
|
||||
// minIndex/maxIndex are computed only on the client-memory array path today, and
|
||||
// xfbCpuCapturedVertices only on the XFB scatter path, so Flags gates the WORK. They
|
||||
// stay in the fixed head; moving them into the variable tail is a wire-format decision
|
||||
// that belongs with the transport (P5), where per-draw byte histograms exist to size
|
||||
// it. userIndices is in the variable tail already, so the VBO path - every Minecraft
|
||||
// and Sodium draw - never pays the 32 bytes of an MGHostSpan.
|
||||
struct MGPDrawInfo {
|
||||
Uint32 Mode;
|
||||
Uint8 IndexSize; // 0 = arrays, else 1 / 2 / 4
|
||||
Uint8 Flags; // MGPDrawFlagBit
|
||||
Uint16 Pad0;
|
||||
Uint32 InstanceCount, StartInstance;
|
||||
Uint32 RestartIndex;
|
||||
Uint32 DrawIdOffset;
|
||||
MGPipeHandle IndexResource;
|
||||
Uint32 MinIndex, MaxIndex; // ~0 = unknown
|
||||
Uint64 XfbCpuCapturedVertices;
|
||||
Uint32 NumDraws; // MGPDrawRange[] in the variable tail
|
||||
Uint32 Pad1;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPDrawInfo, 56);
|
||||
|
||||
// = pipe_draw_start_count_bias.
|
||||
struct MGPDrawRange {
|
||||
Uint32 Start, Count;
|
||||
Int32 IndexBias;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPDrawRange, 12);
|
||||
|
||||
// Present when the draw is indirect. The client resolves the COUNT itself, so the
|
||||
// server never reads an indirect command block to learn how many draws there are.
|
||||
struct MGPDrawIndirect {
|
||||
MGPipeHandle Buffer;
|
||||
MGPipeHandle ParameterBuffer;
|
||||
Uint64 Offset, ParameterOffset;
|
||||
Uint32 Stride, DrawCount;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPDrawIndirect, 40);
|
||||
|
||||
struct MGPGridInfo {
|
||||
Uint32 GridX, GridY, GridZ;
|
||||
Uint32 BlockX, BlockY, BlockZ;
|
||||
MGPipeHandle IndirectBuffer;
|
||||
Uint64 IndirectOffset;
|
||||
Uint8 IsIndirect;
|
||||
Uint8 Pad0[7];
|
||||
};
|
||||
MGP_ASSERT_POD(MGPGridInfo, 48);
|
||||
|
||||
struct MGPMemoryBarrier {
|
||||
Uint32 Bits; // GLbitfield
|
||||
Uint8 ByRegion;
|
||||
Uint8 Pad0[3];
|
||||
};
|
||||
MGP_ASSERT_POD(MGPMemoryBarrier, 8);
|
||||
|
||||
struct MGPStreamOutputBegin {
|
||||
Uint32 PrimitiveMode;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPStreamOutputBegin, 8);
|
||||
|
||||
// end_stream_output carries the accounting the client owns; the scatter itself is a
|
||||
// read-modify-write of the client's shadow and lives there (section 7.2.1).
|
||||
struct MGPXfbAccounting {
|
||||
Uint64 CapturedVertices;
|
||||
Uint64 PrimitivesWritten;
|
||||
Uint32 PrimitiveMode;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPXfbAccounting, 24);
|
||||
|
||||
struct MGPStreamOutputControl {
|
||||
Uint32 Reserved;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPStreamOutputControl, 8);
|
||||
|
||||
struct MGPFlush {
|
||||
Uint32 Flags;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPFlush, 8);
|
||||
|
||||
struct MGPPresent {
|
||||
Uint64 FrameSerial;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPPresent, 8);
|
||||
|
||||
struct MGPSwapInterval {
|
||||
Int32 Interval;
|
||||
Uint32 Pad0;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSwapInterval, 8);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Reverse channel payloads (section 7.1)
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
struct MGPSurfaceInfo {
|
||||
Uint32 Width, Height;
|
||||
Uint32 InternalFormat;
|
||||
Uint16 Samples, Layers;
|
||||
Uint8 IsDefault;
|
||||
Uint8 Pad0[7];
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSurfaceInfo, 24);
|
||||
|
||||
#undef MGP_ASSERT_POD
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user