mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
142
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 |
@@ -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: 7e25545174...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.
|
||||
@@ -495,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;
|
||||
|
||||
@@ -1198,8 +1198,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// Until that reconciliation exists, advertising here would be the same lie the comment
|
||||
// above refuses to tell, just with an extra prerequisite met. Set
|
||||
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
|
||||
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
|
||||
extensions.push_back(E_GL_ARB_texture_view);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
@@ -1255,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;
|
||||
@@ -1417,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
|
||||
@@ -1479,6 +1484,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
|
||||
// extension pairs the loader probed, independently, because they really do come
|
||||
// separately. False arms the shared phase-B demotion
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
|
||||
// never names the built-in in those stages and needs no extension.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
|
||||
// exercised on a healthy driver (the pinned integration lane); =0 restores the
|
||||
// detected answer's declines.
|
||||
m_dynamicParameters.SupportsTessellationPointSize =
|
||||
m_GLESCapabilities.TessellationPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
m_dynamicParameters.SupportsGeometryPointSize =
|
||||
m_GLESCapabilities.GeometryPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = false;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the driver's extensions");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = true;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
|
||||
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
@@ -230,7 +230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
||||
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
||||
//
|
||||
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// Whether emulation runs. Off only under MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
||||
// restores the pre-emulation path as a negative control.
|
||||
Bool ViewportArrayEmulationEnabled();
|
||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
||||
@@ -461,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the owning thread replaying them: guard both fields with pendingMutex.
|
||||
Bool pendingRespecify = false;
|
||||
VecRange1D pendingRanges;
|
||||
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
|
||||
// stage + glCopyBufferSubData at the next sync; see
|
||||
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
|
||||
// the coherent mapping - an in-place host write tears the in-flight frames
|
||||
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
|
||||
struct PendingResidentWrite {
|
||||
SizeT offset = 0;
|
||||
Vector<Uint8> bytes;
|
||||
};
|
||||
Vector<PendingResidentWrite> pendingResidentWrites;
|
||||
std::mutex pendingMutex;
|
||||
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||
@@ -544,12 +554,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// BackendVertexArrayObject::SyncToBackend.
|
||||
extern Uint64 g_bufferBackendIdGeneration;
|
||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
|
||||
// skips the GL call when the (id, range) already at that index matches, like the
|
||||
// array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
|
||||
// (context may reset).
|
||||
// Binds the transform feedback capture points [0, bufferCount) from the frontend
|
||||
// state, and touches nothing else - in particular it never binds a zero the
|
||||
// application did not ask for. See the definition for why that matters on Mali.
|
||||
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
|
||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||
void InvalidateIndexedBufferBindingCache();
|
||||
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
|
||||
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
|
||||
// calls this on each bind/delete.
|
||||
void InvalidateTransformFeedbackBindingShadows();
|
||||
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
||||
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
||||
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
||||
@@ -618,7 +637,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// which is what to watch if this ring ever shows up in an RSS regression: it
|
||||
// grows on demand from 4 MiB and is capped, not unbounded.
|
||||
//
|
||||
// False when the feature is disabled (MOBILEGL_DISABLE_UNPACK_RING),
|
||||
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
|
||||
// EXT_buffer_storage / fences are missing, the ES context is not current, or
|
||||
// ring creation already failed under this context. Callers then upload from
|
||||
// the client pointer exactly as before.
|
||||
@@ -633,6 +652,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Largest single staging request the ring can ever satisfy.
|
||||
SizeT UnpackRingMaxBytes();
|
||||
void UnpackRingOnPresent();
|
||||
|
||||
// --- Buffer upload ring ---------------------------------------------------
|
||||
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
|
||||
// (glBufferSubData / non-persistent map flushes) whose destination store may
|
||||
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
|
||||
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
|
||||
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
|
||||
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
|
||||
// each such call serialized against the whole GPU queue (~1 fps while chunks
|
||||
// stream in, and again on every camera pan). App SubData ranges are queued on
|
||||
// the resource instead (the frontend shadow already holds the bytes) and
|
||||
// draw-time sync drains them: bytes staged into this ring, then one
|
||||
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
|
||||
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
|
||||
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
|
||||
// historical immediate-upload path (negative control / escape hatch).
|
||||
void UploadRingOnPresent();
|
||||
} // namespace BufferImpl
|
||||
|
||||
namespace VertexArrayImpl {
|
||||
@@ -1624,6 +1660,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
const std::map<String, String>& inputBlockRenames,
|
||||
const std::map<String, String>& outputBlockRenames,
|
||||
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
|
||||
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||
String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -41,14 +43,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
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
|
||||
@@ -83,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;
|
||||
@@ -91,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();
|
||||
}
|
||||
@@ -156,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);
|
||||
@@ -169,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;
|
||||
}
|
||||
@@ -183,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;
|
||||
@@ -207,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;
|
||||
@@ -417,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;
|
||||
}
|
||||
|
||||
@@ -532,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;
|
||||
}
|
||||
|
||||
@@ -737,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);
|
||||
|
||||
@@ -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>
|
||||
@@ -125,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
|
||||
@@ -182,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
|
||||
@@ -713,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
|
||||
@@ -2213,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
|
||||
@@ -507,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
|
||||
@@ -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());
|
||||
@@ -624,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
|
||||
@@ -740,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;
|
||||
@@ -785,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();
|
||||
}
|
||||
@@ -939,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);
|
||||
@@ -1081,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);
|
||||
|
||||
@@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
@@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -44,6 +44,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
|
||||
@@ -77,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
// Where a gl_PerVertex built-in output lives, resolved from the module's annotations.
|
||||
// Named for gl_Position because the clip-space fixup is what it was written for, and it
|
||||
// is still the only shape that pass accepts - but the transform-feedback capture pass
|
||||
// resolves gl_PointSize through the same struct, in which case `vectorTypeId` /
|
||||
// `vectorPtrTypeId` hold the SCALAR float type and its Output pointer rather than a vec4.
|
||||
struct PositionTargetInfo {
|
||||
Uint32 variableId = 0;
|
||||
Uint32 vectorTypeId = 0;
|
||||
@@ -102,6 +107,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_PointSize's counterpart to IsVec4Float32. The two are the only shapes any
|
||||
// gl_PerVertex member this file resolves can have, and each resolver takes whichever
|
||||
// one its built-in is declared with, so a mismatched type declines rather than
|
||||
// producing a mirror the driver would reject.
|
||||
Bool IsFloat32Scalar(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||
auto* floatInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
|
||||
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
|
||||
|
||||
if (outFloatTypeId) *outFloatTypeId = typeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Which of the two shapes above a resolver should accept. A plain function pointer
|
||||
// rather than a std::function: every call site is one of the two free functions.
|
||||
using BuiltInTypeCheckFn = Bool (*)(spvtools::opt::IRContext*, Uint32, Uint32*);
|
||||
|
||||
spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) {
|
||||
if (variable.type_description == nullptr) {
|
||||
return SPVC_BASETYPE_UNKNOWN;
|
||||
@@ -381,9 +403,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return used;
|
||||
}
|
||||
|
||||
void ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) {
|
||||
// What a failed validation says, for a caller that wants to put it in its own message.
|
||||
struct SpirvValidationFailure {
|
||||
String message;
|
||||
Int result = 0;
|
||||
SizeT index = 0;
|
||||
};
|
||||
|
||||
// Returns whether the module validates. The result used to be discarded everywhere: the
|
||||
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
|
||||
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
|
||||
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
|
||||
// the same "not a validating entry point" behaviour PipelineFactory already documents for
|
||||
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
|
||||
//
|
||||
// This function does NOT log the failure at E any more. It used to, unlatched, on the
|
||||
// stated grounds that "reaching here already requires the validation switch to be armed,
|
||||
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
|
||||
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
|
||||
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
|
||||
// The failure text now travels back through `outFailure` so the LATCHED call-site
|
||||
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
|
||||
// is the D-level detail and the process-wide counter the test lanes assert on.
|
||||
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
|
||||
SpirvValidationFailure* outFailure = nullptr) {
|
||||
if (outFailure != nullptr) *outFailure = {};
|
||||
if (spirv.empty()) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
spv_const_binary_t binary = {spirv.data(), spirv.size()};
|
||||
@@ -405,18 +451,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
const char* message =
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
|
||||
const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
|
||||
// The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
|
||||
// asserting on the delta). Bumped for every failed validation, including one a
|
||||
// caller goes on to recover from: a transform that produced an invalid module is
|
||||
// a real defect whether or not this run survived it.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_E(
|
||||
if (outFailure != nullptr) {
|
||||
*outFailure = {String(message), static_cast<Int>(result), index};
|
||||
}
|
||||
MGLOG_D(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
index,
|
||||
message);
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
@@ -432,6 +484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
spvValidatorOptionsDestroy(options);
|
||||
spvContextDestroy(context);
|
||||
return result == SPV_SUCCESS;
|
||||
}
|
||||
|
||||
void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable,
|
||||
@@ -743,8 +796,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveDirectBuiltInTarget(spvtools::opt::IRContext* context, Uint32 variableId,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
|
||||
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
|
||||
if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false;
|
||||
@@ -756,7 +809,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
target.variableId = variableId;
|
||||
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
|
||||
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
if (!typeCheck(context, target.vectorTypeId, &target.floatTypeId)) return false;
|
||||
target.vectorPtrTypeId = varInst->type_id();
|
||||
target.isMember = false;
|
||||
|
||||
@@ -771,15 +824,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return context->get_type_mgr()->GetTypeInstruction(&ptrType);
|
||||
}
|
||||
|
||||
Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
PositionTargetInfo* outTarget) {
|
||||
Bool ResolveMemberBuiltInTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
|
||||
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
if (memberIndex >= structInst->NumInOperands()) return false;
|
||||
|
||||
const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
|
||||
Uint32 floatTypeId = 0;
|
||||
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false;
|
||||
if (!typeCheck(context, vectorTypeId, &floatTypeId)) return false;
|
||||
|
||||
const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
|
||||
if (vectorPtrTypeId == 0) return false;
|
||||
@@ -807,27 +860,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
// The OUTPUT variable (or gl_PerVertex member) carrying `builtIn`, if the module
|
||||
// declares one of the expected type. Annotations are the search space deliberately:
|
||||
// they survive the link-time sanitize chain's interface delisting, which is the whole
|
||||
// reason EnsureEntryPointInterface exists.
|
||||
Bool FindBuiltInTarget(spvtools::opt::IRContext* context, spv::BuiltIn builtIn,
|
||||
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
|
||||
Vector<Pair<Uint32, Uint32>> memberCandidates;
|
||||
constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn);
|
||||
constexpr auto kBuiltInPosition = static_cast<Uint32>(spv::BuiltIn::Position);
|
||||
const auto wantedBuiltIn = static_cast<Uint32>(builtIn);
|
||||
|
||||
for (auto& inst : context->module()->annotations()) {
|
||||
if (inst.opcode() == spv::Op::OpDecorate) {
|
||||
if (inst.NumInOperands() < 3) continue;
|
||||
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue;
|
||||
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true;
|
||||
if (inst.GetSingleWordInOperand(2) != wantedBuiltIn) continue;
|
||||
if (ResolveDirectBuiltInTarget(context, inst.GetSingleWordInOperand(0), typeCheck, outTarget)) {
|
||||
return true;
|
||||
}
|
||||
} else if (inst.opcode() == spv::Op::OpMemberDecorate) {
|
||||
if (inst.NumInOperands() < 4) continue;
|
||||
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue;
|
||||
if (inst.GetSingleWordInOperand(3) != wantedBuiltIn) continue;
|
||||
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [structTypeId, memberIndex] : memberCandidates) {
|
||||
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true;
|
||||
if (ResolveMemberBuiltInTarget(context, structTypeId, memberIndex, typeCheck, outTarget)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
|
||||
return FindBuiltInTarget(context, spv::BuiltIn::Position, IsVec4Float32, outTarget);
|
||||
}
|
||||
|
||||
// Put `variableId` back on `entryPoint`'s interface list if it is not already there.
|
||||
//
|
||||
// SPIR-V requires every Input/Output global an entry point statically uses to be listed on
|
||||
// its OpEntryPoint, and spirv-val enforces it ("Interface variable id <N> is used by entry
|
||||
// point 'main' id <M>, but is not listed as an interface"). The link-time sanitize chain
|
||||
// DELISTS a variable nothing referenced yet - ShaderCompiler::SanitizeAndOptimizeBinary
|
||||
// runs CreateAggressiveDCEPass(false), which may never delete an Output, followed by
|
||||
// CreateRemoveUnusedInterfaceVariablesPass, which rebuilds the operand list from the
|
||||
// variables actually referenced. A TES that redeclares `out gl_PerVertex { vec4
|
||||
// gl_Position; }` and never writes it therefore reaches the backend with the OpVariable
|
||||
// and its BuiltIn Position decoration intact and its interface slot gone. Any pass that
|
||||
// then injects a reference has to put the slot back, or it hands the driver a module no
|
||||
// validator accepts - and Mali r54 answers that with a SIGSEGV inside pipeline creation
|
||||
// rather than an error return.
|
||||
//
|
||||
// No SPIR-V version gate here, unlike GlFragCoordYFlipPass's identical call for its
|
||||
// injected PRIVATE global: Input and Output belong on the interface in every version,
|
||||
// and only 1.4 widened it to the other storage classes.
|
||||
Bool EnsureEntryPointInterface(spvtools::opt::IRContext* context, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 variableId) {
|
||||
// In-operands: 0 = execution model, 1 = entry function id, 2 = name, 3.. = interface.
|
||||
constexpr Uint32 kFirstInterfaceOperand = 3;
|
||||
if (variableId == 0) return false;
|
||||
for (Uint32 operand = kFirstInterfaceOperand; operand < entryPoint.NumInOperands(); ++operand) {
|
||||
if (entryPoint.GetSingleWordInOperand(operand) == variableId) return false;
|
||||
}
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
|
||||
context->AnalyzeUses(&entryPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is `pointerId` the position target itself, or an access chain rooted at it?
|
||||
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
|
||||
const PositionTargetInfo& target) {
|
||||
auto* defUse = context->get_def_use_mgr();
|
||||
for (Uint32 current = pointerId; current != 0;) {
|
||||
if (current == target.variableId) return true;
|
||||
const auto* inst = defUse->GetDef(current);
|
||||
if (inst == nullptr) return false;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpPtrAccessChain:
|
||||
case spv::Op::OpInBoundsPtrAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
current = inst->GetSingleWordInOperand(0);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does anything in the module write the position target?
|
||||
//
|
||||
// Deliberately conservative - it answers "assume yes" for every shape it cannot read
|
||||
// exactly, because a false "no" would silently drop the clip-space fixup from a shader
|
||||
// that does write gl_Position, while a false "yes" only reinstates the behaviour this
|
||||
// pass has always had. Scans every function rather than just the entry point's: a shader
|
||||
// that assigns gl_Position inside a helper is still a shader that writes it, and passing
|
||||
// the pointer to a call is a write as far as this can tell.
|
||||
Bool ModuleWritesPositionTarget(spvtools::opt::IRContext* context, const PositionTargetInfo& target) {
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (const auto& inst : block) {
|
||||
switch (inst.opcode()) {
|
||||
case spv::Op::OpStore:
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(0), target)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpFunctionCall:
|
||||
// In-operand 0 is the callee; the rest are arguments.
|
||||
for (Uint32 argument = 1; argument < inst.NumInOperands(); ++argument) {
|
||||
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(argument),
|
||||
target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -914,6 +1070,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
|
||||
|
||||
// Nothing to remap in a Position the shader never writes. Declining is not just
|
||||
// an optimisation: the fixup is load-modify-store, so on an unwritten Position it
|
||||
// converts "undefined, never written" into "written with whatever the load
|
||||
// returned", and the store is a reference to a variable the link-time sanitize
|
||||
// chain has already delisted from the entry-point interface. glslang emits the
|
||||
// OpVariable for every DECLARED interface block, so a redeclared-but-unwritten
|
||||
// `out gl_PerVertex` is a shape real shaders have.
|
||||
if (!ModuleWritesPositionTarget(context(), target)) {
|
||||
MGLOG_D("gl-to-vulkan-position-fix: the shader never writes gl_Position; leaving it alone");
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
|
||||
if (!floatType) return Status::SuccessWithoutChange;
|
||||
|
||||
@@ -945,6 +1113,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
|
||||
if (!function) continue;
|
||||
|
||||
Bool modifiedThisEntryPoint = false;
|
||||
for (auto& bb : *function) {
|
||||
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
|
||||
auto* inst = &*instIter;
|
||||
@@ -953,10 +1122,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
|
||||
if (!needsFixup) continue;
|
||||
|
||||
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
modifiedThisEntryPoint |=
|
||||
InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
|
||||
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
|
||||
}
|
||||
}
|
||||
// Per entry point, and only for one this pass actually injected into: the
|
||||
// injected load/store is a static use of the position variable, so the
|
||||
// variable has to be on THIS entry point's interface list.
|
||||
if (modifiedThisEntryPoint) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
modified |= modifiedThisEntryPoint;
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1235,6 +1412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool needsPositionMirror = false;
|
||||
Uint32 positionBufferIndex = 0;
|
||||
Uint32 positionOffset = 0;
|
||||
// gl_PointSize is a gl_PerVertex MEMBER, never a variable of its own, so the
|
||||
// debug-name lookup below can never resolve it - it used to fall through to
|
||||
// "no SPIR-V variable named 'gl_PointSize'" and leave the frontend's reserved
|
||||
// slot unwritten, or, when it was the only capture, leave the module with no
|
||||
// Xfb execution mode at all and the whole span declined.
|
||||
Bool needsPointSizeMirror = false;
|
||||
Uint32 pointSizeBufferIndex = 0;
|
||||
Uint32 pointSizeOffset = 0;
|
||||
for (const auto& varying : m_varyings) {
|
||||
if (varying.name == "gl_Position") {
|
||||
needsPositionMirror = true;
|
||||
@@ -1242,6 +1427,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
positionOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.name == "gl_PointSize") {
|
||||
// A demoted module (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
|
||||
// built-in member - the value lives in the carrier variable the demotion
|
||||
// named - so the capture binds to the carrier directly. The mirror below
|
||||
// must not run for it: reading the now-unwritten member would capture
|
||||
// garbage, and the read itself is the capability access the demotion
|
||||
// exists to remove. Detected off the module's own debug names, so a
|
||||
// composite built from another program's stage answers for the module it
|
||||
// actually contains.
|
||||
const auto carrierIt = idsByName.find(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
|
||||
if (carrierIt != idsByName.end()) {
|
||||
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
continue;
|
||||
}
|
||||
needsPointSizeMirror = true;
|
||||
pointSizeBufferIndex = varying.bufferIndex;
|
||||
pointSizeOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
if (varying.blockMemberIndex >= 0) {
|
||||
// glslang names the block's instance variable and its struct type
|
||||
// separately; an anonymous instance leaves only the type named, so
|
||||
@@ -1306,8 +1513,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::Position, IsVec4Float32,
|
||||
"gl_Position", positionBufferIndex, positionOffset,
|
||||
decorateForXfb);
|
||||
}
|
||||
if (needsPointSizeMirror) {
|
||||
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
|
||||
spv::BuiltIn::PointSize, IsFloat32Scalar,
|
||||
"gl_PointSize", pointSizeBufferIndex,
|
||||
pointSizeOffset, decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
@@ -1347,19 +1562,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// gl_Position and gl_PointSize are captured the same way and differ only in which
|
||||
// built-in is looked up and what type it has, so one injector serves both. Anything
|
||||
// else in gl_PerVertex would need its own type check before it could be added here.
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||
Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
|
||||
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
|
||||
const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
|
||||
if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: %s capture requested but no such output found", glslName);
|
||||
return false;
|
||||
}
|
||||
if (!target.isMember) {
|
||||
// Standalone gl_Position variable: decorate it directly.
|
||||
// Standalone built-in variable: decorate it directly. It still has to be
|
||||
// on the interface - a transform-feedback decoration on a variable the entry
|
||||
// point does not list captures nothing, and the sanitize chain delists an
|
||||
// unwritten one (see EnsureEntryPointInterface).
|
||||
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1413,6 +1637,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
injected = true;
|
||||
}
|
||||
}
|
||||
// The mirror was listed on the entry point above, but the loop just added a READ
|
||||
// of the SOURCE block through an access chain, and the interface rule covers
|
||||
// reads exactly as it covers writes. A built-in capture on a shader whose
|
||||
// block the sanitize chain delisted - a TES that redeclares `out gl_PerVertex`
|
||||
// and never writes it, which is what the tessellation_control_to_tessellation_
|
||||
// evaluation.gl_MaxPatchVertices_Position_PointSize bodies do - produced an
|
||||
// invalid module here for the same reason the position fixup did.
|
||||
if (injected) {
|
||||
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
|
||||
@@ -3236,11 +3470,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
if (program.PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
|
||||
// rendering assertion above it stays green on a healthy driver whether the
|
||||
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
|
||||
"demoted to an ordinary varying, because this device cannot host the built-in "
|
||||
"in those stages.");
|
||||
}
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
if (enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
||||
// not only when the switch is armed, so the validator's static tables have to be pinned
|
||||
// against process exit in every build too.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
||||
|
||||
@@ -3264,6 +3508,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
|
||||
// These two passes INJECT references - a store for the clip fixup, an access
|
||||
// chain and a load for the gl_Position capture mirror - and a reference to a
|
||||
// variable the link-time sanitize chain delisted from the entry-point interface
|
||||
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
|
||||
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
|
||||
// is the backstop.
|
||||
//
|
||||
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
|
||||
// are not equally optional. Rewinding straight to `spv` would also throw away the
|
||||
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
|
||||
// Xfb execution mode - while the renderer decides to call
|
||||
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
|
||||
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
|
||||
// into a transform-feedback span, violating
|
||||
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
|
||||
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
|
||||
// capture working and costs only the clip-space remap.
|
||||
//
|
||||
// Once per program on a cache miss, and only for the single stage that carries the
|
||||
// fixups - not per draw and not per module.
|
||||
SpirvValidationFailure fixupFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
|
||||
&fixupFailure)) {
|
||||
SpirvValidationFailure xfbFailure{};
|
||||
if (fixupInput != &spv &&
|
||||
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the capture-decorated one, so this program draws "
|
||||
"without the clip-space remap",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = *fixupInput;
|
||||
} else {
|
||||
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
|
||||
"stage %d (%s); keeping the untransformed one",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
fixupFailure.message.c_str());
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
@@ -3472,15 +3756,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only
|
||||
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
|
||||
// by here the descriptor bindings have been remapped and the layout about to be
|
||||
// reflected describes the remapped module, so there is no module left that is both
|
||||
// valid and consistent with it to fall back to. The recovery lives one step earlier,
|
||||
// at the clip/XFB fixups (see the revert there) - which is where a transform can
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
// varying and changed nothing, anything later that might strip it. The renderer picks
|
||||
// its capture commands from GL state alone and would otherwise open a span against a
|
||||
// pipeline that cannot feed it.
|
||||
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0 &&
|
||||
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
|
||||
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
|
||||
"than recorded against a pipeline that cannot feed them",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.xfbCaptureDeclined = true;
|
||||
}
|
||||
|
||||
// Does this stage need a device feature the device did not give us? Asked ONLY when
|
||||
// the feature is off, so a device that has it - the common case - pays nothing: the
|
||||
// whole test is short-circuited before the module is parsed.
|
||||
//
|
||||
// gl_PointSize is an ordinary per-vertex output in desktop GL and any
|
||||
// vertex-processing stage may write it, but Vulkan puts the built-in behind
|
||||
// shaderTessellationAndGeometryPointSize in the tessellation and geometry stages
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). glslang emits TessellationPointSize /
|
||||
// GeometryPointSize from the application's own access, so this program is legal GL
|
||||
// that this device cannot run - the same shape the DirectGLES arm reports when a
|
||||
// driver advertises neither EXT nor OES point-size extension, and it deserves the
|
||||
// same named message rather than a pipeline the driver may fault on.
|
||||
if (!m_tessellationAndGeometryPointSizeEnabled &&
|
||||
(stages[i] == ShaderStage::TessControl || stages[i] == ShaderStage::TessEval ||
|
||||
stages[i] == ShaderStage::Geometry) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(
|
||||
moduleSpv)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: program %u stage %d accesses gl_PointSize, but this device does not "
|
||||
"support shaderTessellationAndGeometryPointSize; its draws are refused rather than "
|
||||
"built into a pipeline the driver may fault on. Point size from a non-vertex stage "
|
||||
"is not available on this device.",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]));
|
||||
entry.pointSizeCapabilityUnsupported = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = moduleSpv.size() * sizeof(Uint);
|
||||
@@ -3758,11 +4090,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
|
||||
// links the program at both 430 and 460.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
// Only gl_Position is written, and gl_PointSize is declared without being forwarded. That
|
||||
// is a KNOWN GAP, not a design: GL 4.6 core 11.2.2 says the fixed-function pass-through
|
||||
// hands the input patch to the evaluation stage unmodified, so an evaluation stage
|
||||
// reading gl_in[].gl_PointSize should see the vertex stage's value and instead sees
|
||||
// whatever this stage left in gl_out[] - which is nothing. A capture of it (the mirror in
|
||||
// XfbCaptureDecoratePass) faithfully records that nothing.
|
||||
//
|
||||
// The reason this comment used to give - "the renderer does not enable
|
||||
// shaderTessellationAndGeometryPointSize" - stopped being true when
|
||||
// VulkanRenderer::CreateLogicalDeviceAndQueues started taking the feature wherever the
|
||||
// device advertises it. Closing the gap is therefore possible now, but it is not free:
|
||||
// the forwarding store has to be gated on that feature, because on a device without it
|
||||
// the store is exactly the invalid usage the build-time refusal
|
||||
// (VkProgramObject::pointSizeCapabilityUnsupported) exists to keep away from the driver -
|
||||
// and this synthesized stage is not the application's, so refusing the program because
|
||||
// MobileGL's own pass-through named a built-in would be the wrong trade. Nothing pins
|
||||
// the shape either: every case in TessellationXfbCaptureScenario builds an explicit
|
||||
// control stage, so a TES-without-TCS test has to come with the fix.
|
||||
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
|
||||
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
|
||||
@@ -3862,14 +4207,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
{
|
||||
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
|
||||
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
|
||||
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
|
||||
// shipping build can be handed by an application. The message is latched all the same
|
||||
// - the pass-through cache is keyed on patchVertices, so a broken template would
|
||||
// otherwise re-report once per distinct patch size.
|
||||
Bool validateThisOne = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
validateThisOne = true;
|
||||
#else
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
validateThisOne = m_enableSpirvValidation;
|
||||
if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
#endif
|
||||
SpirvValidationFailure passthroughFailure{};
|
||||
if (validateThisOne &&
|
||||
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
|
||||
"patchVertices=%u does not validate (%s)",
|
||||
patchVertices, passthroughFailure.message.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
|
||||
@@ -202,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
// The program has a tessellation or geometry module declaring TessellationPointSize /
|
||||
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
|
||||
// is off, so a pipeline built from it is invalid usage
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
|
||||
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
|
||||
// implements one level up, and the counterpart of the DirectGLES arm that reports a
|
||||
// driver with neither point-size extension by name.
|
||||
//
|
||||
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
@@ -426,12 +452,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool tessellationAndGeometryPointSizeEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
@@ -591,6 +619,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
|
||||
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
|
||||
// GeometryPointSize is refused at build time (see VkProgramObject::
|
||||
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#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"
|
||||
|
||||
@@ -54,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
|
||||
|
||||
@@ -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) {
|
||||
@@ -610,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])));
|
||||
@@ -962,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;
|
||||
@@ -1108,7 +1109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -805,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 {
|
||||
@@ -948,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 =
|
||||
@@ -3150,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;
|
||||
|
||||
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,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
|
||||
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
|
||||
// and capturable by name through transform feedback). Cached at device creation and
|
||||
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
|
||||
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
|
||||
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
|
||||
// that is invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
|
||||
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
|
||||
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
|
||||
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
|
||||
@@ -666,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{};
|
||||
@@ -709,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;
|
||||
@@ -768,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
|
||||
@@ -812,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
|
||||
@@ -823,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
|
||||
@@ -921,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
|
||||
@@ -1279,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 {
|
||||
@@ -146,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;
|
||||
}
|
||||
}
|
||||
@@ -172,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:
|
||||
@@ -381,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;
|
||||
@@ -497,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);
|
||||
}
|
||||
|
||||
@@ -505,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);
|
||||
}
|
||||
|
||||
@@ -514,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);
|
||||
}
|
||||
|
||||
@@ -523,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);
|
||||
}
|
||||
@@ -532,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);
|
||||
}
|
||||
|
||||
@@ -540,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);
|
||||
}
|
||||
|
||||
@@ -549,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);
|
||||
}
|
||||
|
||||
@@ -558,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);
|
||||
}
|
||||
|
||||
@@ -566,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);
|
||||
}
|
||||
|
||||
@@ -575,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);
|
||||
}
|
||||
@@ -585,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);
|
||||
}
|
||||
@@ -595,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);
|
||||
}
|
||||
@@ -605,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);
|
||||
}
|
||||
|
||||
@@ -615,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);
|
||||
}
|
||||
@@ -625,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);
|
||||
}
|
||||
@@ -635,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);
|
||||
}
|
||||
@@ -645,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);
|
||||
}
|
||||
|
||||
@@ -653,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,
|
||||
@@ -661,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);
|
||||
}
|
||||
@@ -670,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);
|
||||
}
|
||||
|
||||
@@ -678,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);
|
||||
}
|
||||
|
||||
@@ -709,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);
|
||||
}
|
||||
|
||||
@@ -761,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -782,6 +836,7 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -852,6 +907,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
@@ -873,6 +929,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -886,6 +943,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support regional memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrierByRegion);
|
||||
memoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
@@ -1208,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);
|
||||
}
|
||||
}
|
||||
@@ -1290,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();
|
||||
@@ -1298,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);
|
||||
}
|
||||
}
|
||||
@@ -1319,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();
|
||||
}
|
||||
}
|
||||
@@ -1333,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();
|
||||
}
|
||||
}
|
||||
@@ -1538,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);
|
||||
@@ -1569,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -616,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);
|
||||
}
|
||||
@@ -629,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);
|
||||
}
|
||||
@@ -640,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);
|
||||
}
|
||||
|
||||
@@ -650,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);
|
||||
}
|
||||
|
||||
@@ -660,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);
|
||||
}
|
||||
|
||||
@@ -670,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);
|
||||
}
|
||||
|
||||
@@ -2729,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);
|
||||
}
|
||||
|
||||
@@ -2994,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,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
|
||||
@@ -1173,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);
|
||||
@@ -1353,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();
|
||||
}
|
||||
}
|
||||
@@ -2263,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,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*
|
||||
@@ -3398,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -64,21 +65,43 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// and none of them has any state beyond "which object is counting".
|
||||
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
|
||||
|
||||
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
|
||||
// ADVERTISED list rather than from a capability bit for the same reason
|
||||
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
|
||||
// support" and "the tessellation-conditional API surface is open" the same fact by
|
||||
// construction, so the day a backend starts advertising the string the surface below opens
|
||||
// with it and no second edit is owed.
|
||||
Bool AdvertisesTessellationShaderExtension() {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) return false;
|
||||
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
|
||||
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
|
||||
}
|
||||
|
||||
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
|
||||
// A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core
|
||||
// since 4.6 and there is no query by which an application could learn otherwise before
|
||||
// calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1
|
||||
// provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the
|
||||
// spec's own signal that the counter is unsupported and its results indeterminate. That
|
||||
// is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a
|
||||
// core entry point, is both non-conformant AND less informative.
|
||||
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
|
||||
// by which an application could learn otherwise before calling. MobileGL instruments none of
|
||||
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
|
||||
// zero for these targets, which is the spec's own signal that the counter is unsupported and
|
||||
// its results indeterminate. That is an honest zero, not an advertised capability - the
|
||||
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
|
||||
// informative.
|
||||
//
|
||||
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
|
||||
// makes them conditional on tessellation support rather than unconditional, and the only
|
||||
// thing an application (or the conformance suite) can read to decide whether an
|
||||
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
|
||||
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
|
||||
// tessellation-conditional token while withholding the string that announces the condition
|
||||
// is self-contradictory, and it is the contradiction the suite catches
|
||||
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
|
||||
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
|
||||
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
|
||||
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
|
||||
switch (target) {
|
||||
case GL_VERTICES_SUBMITTED:
|
||||
case GL_PRIMITIVES_SUBMITTED:
|
||||
case GL_VERTEX_SHADER_INVOCATIONS:
|
||||
case GL_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
|
||||
case GL_FRAGMENT_SHADER_INVOCATIONS:
|
||||
@@ -86,6 +109,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_CLIPPING_INPUT_PRIMITIVES:
|
||||
case GL_CLIPPING_OUTPUT_PRIMITIVES:
|
||||
return true;
|
||||
case GL_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
return AdvertisesTessellationShaderExtension();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -139,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;
|
||||
@@ -153,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;
|
||||
@@ -232,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
|
||||
@@ -246,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;
|
||||
@@ -261,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;
|
||||
}
|
||||
@@ -272,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
|
||||
@@ -292,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;
|
||||
@@ -397,6 +430,7 @@ 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;
|
||||
@@ -416,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;
|
||||
@@ -494,6 +529,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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);
|
||||
@@ -502,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;
|
||||
}
|
||||
@@ -554,6 +592,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
MGP_FILL(EndXfbPrimitivesQuery);
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
}
|
||||
@@ -563,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;
|
||||
@@ -579,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;
|
||||
@@ -617,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;
|
||||
@@ -746,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;
|
||||
@@ -887,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <MG_Impl/GLImpl/Sampler/Validators.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -1076,6 +1077,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Vector<Uint8> scratch(static_cast<SizeT>(width) * static_cast<SizeT>(height) * bytesPerTexel);
|
||||
{
|
||||
ScopedNeutralPackState neutralPack;
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data());
|
||||
}
|
||||
|
||||
@@ -1619,6 +1621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GenerateMipmap_Backend(GLenum target) {
|
||||
MGP_FILL(GenerateMipmap);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
|
||||
}
|
||||
|
||||
@@ -4024,6 +4027,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexSubImage2D_Backend(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height) {
|
||||
MGP_FILL(CopyTexSubImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
@@ -4040,6 +4044,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support image-to-image copies."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(CopyImageSubData);
|
||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
@@ -4461,6 +4466,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexImage2D_Backend(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
MGP_FILL(CopyTexImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D(target, level, internalformat, x, y, width, height,
|
||||
border);
|
||||
}
|
||||
@@ -5071,6 +5077,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetTexImage_Backend(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
MGP_FILL(GetTexImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
|
||||
@@ -5078,9 +5085,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The half of the GetTexImage/GetTextureImage error set (GL 4.6 core 8.11) that depends on the
|
||||
// resolved texture object rather than on how it was named. Shared because the by-name entry
|
||||
// point does not route through GetTexImage_State and so used to enforce none of it.
|
||||
// A cube map's six faces are six independent images, and both readback spellings name one of
|
||||
// them: glGetTexImage through the TARGET token, glGetTextureSubImage through zoffset. Both then
|
||||
// have to tell the size checks below that ONE image is coming back, not six.
|
||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||
return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
}
|
||||
|
||||
// `imagesQueried` is how many of the texture's upload-target images the query hands back, and
|
||||
// exists for the destination-size check at the bottom. Zero means "all of them", which is what
|
||||
// the whole-level forms return - every face of a cube map. glGetTextureSubImage naming ONE cube
|
||||
// face passes 1: sizing that request against six faces' worth would reject the only buffer a
|
||||
// single-face read has any reason to pass.
|
||||
Bool ValidateTextureImageQuery(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
|
||||
TextureInputFormat textureInputFormat, TexturePixelDataType texturePixelDataType,
|
||||
GLsizei bufSize, const void* pixels, const char* caller) {
|
||||
GLsizei bufSize, const void* pixels, const char* caller,
|
||||
SizeT imagesQueried = 0) {
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -5196,12 +5216,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tightly packed, and summed over every face because a cube map query returns all
|
||||
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
||||
// could not have fit under any packing.
|
||||
// Tightly packed, and summed over every face because a whole-level cube map query
|
||||
// returns all six - unless the caller named a single face, which is what a non-zero
|
||||
// imagesQueried says. Pack pixel-store state only ever grows this, so a request
|
||||
// rejected here could not have fit under any packing.
|
||||
const SizeT imageCount = imagesQueried != 0 ? imagesQueried : uploadTargets.size();
|
||||
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||
texturePixelDataType, texelSize) *
|
||||
uploadTargets.size();
|
||||
imageCount;
|
||||
|
||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -5274,9 +5296,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit".
|
||||
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit". That skips
|
||||
// the destination-size branch but NOT the pixel-pack-buffer one, which measures the same
|
||||
// `required` against the bound PBO's real size - so a cube FACE query has to say it returns
|
||||
// one image here too, or a PBO sized for the one face this call packs is refused as too
|
||||
// small while the copy that follows writes exactly that much into it.
|
||||
return ValidateTextureImageQuery(textureObject, level, textureInputFormat, texturePixelDataType, -1, pixels,
|
||||
"GetTexImage_State");
|
||||
"GetTexImage_State",
|
||||
IsCubeMapFaceUploadTarget(textureUploadTarget) ? 1u : 0u);
|
||||
}
|
||||
|
||||
// What this helper can and cannot answer.
|
||||
@@ -6423,6 +6450,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The half glGetTextureImage and glGetTextureSubImage share: which of the two readbacks answers,
|
||||
// for ONE named upload target. Factored out so the sub-image form can name a cube FACE - the
|
||||
// by-name spelling of the face token glGetTexImage takes - instead of re-deriving the target and
|
||||
// silently landing on the +X face the way the delegation it replaces did.
|
||||
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
@@ -6431,16 +6476,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
__func__)) {
|
||||
return;
|
||||
}
|
||||
const auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels,
|
||||
__func__);
|
||||
GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
|
||||
bufSize, pixels, __func__);
|
||||
}
|
||||
|
||||
void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels) {
|
||||
@@ -6484,9 +6521,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && zoffset == 0 &&
|
||||
width == texelSize.x() && height == texelSize.y() &&
|
||||
depth == texelSize.z();
|
||||
// On a cube map, z is the FACE axis. A cube map's level is stored per face, so its level
|
||||
// size reads z = 1 whichever face named it - but GL 4.6 core 8.11.4 addresses the six faces
|
||||
// of a cube map through zoffset/depth, exactly the six layers a face token names for
|
||||
// glGetTexImage. Without this arm the z range was measured against that 1 and only zoffset 0
|
||||
// (the +X face) was expressible; the other five were rejected as a partial read.
|
||||
//
|
||||
// Only ONE face at a time. depth > 1 would have to concatenate faces into the destination,
|
||||
// which is the same unimplemented multi-image packing the check below still refuses.
|
||||
const Bool isSingleCubeFaceRead = textureObject->GetTarget() == TextureTarget::TextureCubeMap &&
|
||||
depth == 1 && zoffset < 6;
|
||||
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && width == texelSize.x() &&
|
||||
height == texelSize.y() &&
|
||||
(isSingleCubeFaceRead || (zoffset == 0 && depth == texelSize.z()));
|
||||
if (!isFullLevelRead) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -6495,7 +6542,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
GetTextureImage(texture, level, format, type, bufSize, pixels);
|
||||
const TextureUploadTarget readUploadTarget =
|
||||
isSingleCubeFaceRead ? static_cast<TextureUploadTarget>(
|
||||
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + zoffset)
|
||||
: uploadTarget;
|
||||
if (!ValidateTextureImageQuery(textureObject, level, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type), bufSize, pixels, __func__,
|
||||
isSingleCubeFaceRead ? 1u : 0u)) {
|
||||
return;
|
||||
}
|
||||
GetTextureImageForUploadTarget(textureObject, readUploadTarget, level, format, type, bufSize, pixels,
|
||||
__func__);
|
||||
}
|
||||
|
||||
// A buffer texture carries none of the sampler or level state these queries report. Reached by
|
||||
@@ -6608,6 +6665,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
|
||||
.Bind(textureObject, level, layered, layer, access, format);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
MGP_FILL(BindImageTexture);
|
||||
bindImageTexture(unit, texture, level, layered, layer, access, format);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +59,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DrawParametersScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
Scenarios/SampleMaskScopeScenario.cpp
|
||||
Scenarios/SampledSetStalenessScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/SnormAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
@@ -88,6 +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
|
||||
@@ -97,15 +102,21 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
||||
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||
Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||
Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
Scenarios/PointSizeDemotionScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/CopyImagePacked16Scenario.cpp
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredAttachmentShapeScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/LargeArenaAdoptionScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/SpirvShaderBinaryScenario.cpp
|
||||
@@ -115,6 +126,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/PipeVerifyArmingScenario.cpp
|
||||
Scenarios/PoisonOmissionScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -280,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()
|
||||
|
||||
@@ -345,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)
|
||||
@@ -412,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
|
||||
@@ -508,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"
|
||||
|
||||
@@ -378,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,375 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
|
||||
//
|
||||
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
|
||||
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
|
||||
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
|
||||
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
|
||||
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
|
||||
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
|
||||
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
|
||||
// texel-block move - between a mirrored allocation and a plain one lands the fields
|
||||
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
|
||||
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
|
||||
// image are clean (the driver decodes its own layout consistently), which is why only the
|
||||
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
|
||||
// checks always passed.
|
||||
//
|
||||
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
|
||||
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
|
||||
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
|
||||
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
|
||||
// the driver's layout.
|
||||
//
|
||||
// The repair under test is the packed16 storage widening
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
|
||||
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
|
||||
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
|
||||
// encodes to UNorm8 and back losslessly for every n <= 8.
|
||||
//
|
||||
// This scenario runs in BOTH configurations, and both must hand back identical client words:
|
||||
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
|
||||
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
|
||||
// covered);
|
||||
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
|
||||
// which is the storage every affected device will actually run - without it the repair
|
||||
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
|
||||
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
|
||||
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
|
||||
// client-invisible: same words in, same words out, on every leg the failing bodies used.
|
||||
//
|
||||
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
|
||||
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
|
||||
// means the widening (or the narrow path it replaces) is.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kBaseSize = 30; // array level 0; level 1 is 15x15
|
||||
constexpr int kLevel1Size = kBaseSize / 2;
|
||||
constexpr int kLayers = 12;
|
||||
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
|
||||
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
|
||||
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
|
||||
constexpr int kRegion = kFlatSize;
|
||||
constexpr int kArrayOffset = 8;
|
||||
|
||||
struct PackedFormatCase {
|
||||
GLenum internalFormat; // the spelling the CTS uses
|
||||
GLenum transferFormat;
|
||||
GLenum transferType;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
// Per-texel varying words, every field inside its width, so a swapped field order (or
|
||||
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
|
||||
GLushort MakeWord(GLenum type, int i) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
|
||||
return static_cast<GLushort>((r << 11) | (g << 5) | b);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4: {
|
||||
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
|
||||
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
|
||||
}
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1: {
|
||||
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
|
||||
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
|
||||
std::vector<GLushort> words(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
class CopyImagePacked16Scenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
|
||||
// every odd-width row of the 15x15 level and shear the comparisons.
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 2);
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
|
||||
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
|
||||
// below can pass by reading a level that was never written.
|
||||
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
|
||||
const std::vector<GLushort>& level1) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
|
||||
kLayers, 0, format.transferFormat, format.transferType, level0.data());
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
|
||||
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
|
||||
const int level2Size = kLevel1Size / 2;
|
||||
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
|
||||
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
|
||||
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
|
||||
// every assertion reads.
|
||||
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
|
||||
format.transferFormat, format.transferType, texels.data());
|
||||
for (int level = 1; level <= 2; ++level) {
|
||||
const int size = std::max(kFlatSize >> level, 1);
|
||||
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
|
||||
format.transferFormat, format.transferType, fill.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
|
||||
const PackedFormatCase& format, size_t texelCount) {
|
||||
std::vector<GLushort> words(texelCount, 0);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
|
||||
glBindTexture(target, 0);
|
||||
return words;
|
||||
}
|
||||
|
||||
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
|
||||
// image equals the corresponding source word, and every word outside it still holds
|
||||
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
|
||||
// regression into a one-line diagnosis.
|
||||
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
|
||||
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
|
||||
const std::vector<GLushort>& fill, const char* what) {
|
||||
for (int y = 0; y < width; ++y) {
|
||||
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
|
||||
const bool inRegion =
|
||||
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
|
||||
const GLushort actual = got[static_cast<size_t>(y * width + x)];
|
||||
const GLushort expected =
|
||||
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
|
||||
(x - x0))]
|
||||
: fill[static_cast<size_t>(y * width + x)];
|
||||
EXPECT_EQ(actual, expected)
|
||||
<< what << ": texel (" << x << ", " << y << ")"
|
||||
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
|
||||
<< ", expected 0x" << expected;
|
||||
if (actual != expected) return; // one texel names the defect; 224 more would bury it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
const PackedFormatCase kFormats[] = {
|
||||
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
|
||||
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
|
||||
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
|
||||
};
|
||||
|
||||
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
|
||||
// the array-as-destination direction of 12 of the 18 failing bodies.
|
||||
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint source = MakeFlatTexture(format, flat);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
|
||||
kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
|
||||
(std::string("2d->2d_array level 1, ") + format.name).c_str());
|
||||
// The source must not have moved - the CTS asserts this before it ever looks at
|
||||
// the destination, and it is what pins the corruption to the copy itself.
|
||||
const auto sourceAfter =
|
||||
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
|
||||
(std::string("source after 2d->2d_array, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
|
||||
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
|
||||
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
const GLuint destination = MakeFlatTexture(format, fill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
|
||||
static_cast<size_t>(kFlatSize) * kFlatSize);
|
||||
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
|
||||
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
|
||||
// that requires the renderbuffer's ES storage to move together with the textures' -
|
||||
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
|
||||
// reached textures alone would break exactly here.
|
||||
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
for (const PackedFormatCase& format : kFormats) {
|
||||
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
|
||||
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
|
||||
const GLuint array = MakeArrayTexture(format, level0, level1);
|
||||
|
||||
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< format.name << ": the renderbuffer is not attachable";
|
||||
// Field values picked to encode exactly in the narrow fields AND in their
|
||||
// UNorm8 expansions, so the expected word is the same whichever storage the
|
||||
// configuration picked - which is the point of the whole scenario.
|
||||
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
|
||||
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
|
||||
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
|
||||
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
|
||||
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
|
||||
static_cast<GLfloat>(max - 2) / max, 1.0f};
|
||||
// The context is shared with every scenario in this process; a scissor left on
|
||||
// would clip the clear and hand the copy undefined renderbuffer texels.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearBufferfv(GL_COLOR, 0, clearColor);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
|
||||
|
||||
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
|
||||
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< format.name << ": glCopyImageSubData raised an error";
|
||||
|
||||
GLushort clearedWord = 0;
|
||||
switch (format.transferType) {
|
||||
case GL_UNSIGNED_SHORT_5_6_5:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1:
|
||||
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4:
|
||||
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
|
||||
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
|
||||
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
|
||||
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
|
||||
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,238 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
|
||||
//
|
||||
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
|
||||
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
|
||||
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
|
||||
// the capability IS there both backends translate the factors properly, and that has always
|
||||
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
|
||||
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
|
||||
// application asking for a blend factor the device cannot do is a picture problem, never a reason
|
||||
// to take the process down.
|
||||
//
|
||||
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
|
||||
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
|
||||
// scenario pins that it lands on one of them and never on a crash:
|
||||
//
|
||||
// capability present - src0 * src1 + dst * (1 - src1)
|
||||
// capability absent - src0, written straight through
|
||||
//
|
||||
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
|
||||
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
|
||||
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
|
||||
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
|
||||
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
|
||||
// scenario exists for - is unit-tested against stubbed capabilities in
|
||||
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
|
||||
// which is the only place it can be reached without a driver that lacks the extension.
|
||||
//
|
||||
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
|
||||
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
|
||||
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
|
||||
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
|
||||
// would still be invalid pipeline state.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
|
||||
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
|
||||
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
|
||||
// harness's compile-and-link helper.
|
||||
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uSrc0;
|
||||
uniform vec4 uSrc1;
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main()
|
||||
{
|
||||
fragColor0 = uSrc0;
|
||||
fragColor1 = uSrc1;
|
||||
}
|
||||
)";
|
||||
|
||||
class DualSourceBlendScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
|
||||
m_programError = error;
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// Capability probe, not an assertion. A GL link that succeeded is not proof that
|
||||
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
|
||||
// use, and GLSL ES has no `index` layout qualifier outside
|
||||
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
|
||||
// and the draw renders nothing. One unblended white draw tells the two apart, and
|
||||
// the cases skip rather than measure a picture the driver never produced.
|
||||
if (m_program != 0) {
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
|
||||
glFinish();
|
||||
const Image probe = ReadPixels(kExtent, kExtent);
|
||||
m_programRenders =
|
||||
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
|
||||
}
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
}
|
||||
|
||||
void Draw(float src0, float src1) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
// Both cases share this gate: nothing below can be measured on a backend that cannot
|
||||
// run a dual-source fragment program at all. Returns the skip reason, empty when the
|
||||
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
|
||||
// expands to a `return` and would leave only the HELPER, letting the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyTheProgramCannotRun() const {
|
||||
if (m_program == 0) {
|
||||
return "this driver cannot build a dual-source fragment shader: " + m_programError;
|
||||
}
|
||||
if (!m_programRenders) {
|
||||
return "this backend links a dual-source fragment program but renders nothing with it "
|
||||
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
bool m_programRenders = false;
|
||||
std::string m_programError;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The whole point of the scenario: this sequence used to be a process kill on any device
|
||||
// without the capability, and it has to be a picture either way.
|
||||
//
|
||||
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
|
||||
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
|
||||
// declined = 1.0 -> 255
|
||||
// Anything else means the factors were mistranslated rather than either honoured or declined.
|
||||
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
|
||||
glFinish();
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
const int red = static_cast<int>(centre.r);
|
||||
const bool blended = red > 100 && red < 160;
|
||||
const bool declined = red > 245;
|
||||
EXPECT_TRUE(blended || declined)
|
||||
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
|
||||
<< "straight-through source (255) - the SRC1 factors were mistranslated";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
|
||||
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
|
||||
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
|
||||
// source either way, so this case is really "no crash, no error, no surprise".
|
||||
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(static_cast<int>(centre.r), 245)
|
||||
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -137,7 +137,7 @@ namespace MGITest {
|
||||
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
||||
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
|
||||
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
|
||||
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
|
||||
// the pack itself replays through the MagmaFixIterationRPSubgroupScratch patch, which
|
||||
// this probe deliberately does not model. The width only gates the domain;
|
||||
// lane placement and group counts still come from observed values alone.
|
||||
bool SubgroupWidthInSourceDomain() const {
|
||||
|
||||
@@ -0,0 +1,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
|
||||
@@ -32,14 +32,20 @@
|
||||
// too, so the per-slice branch that exists for exactly this case was unreachable and every
|
||||
// slice above z = 0 came back VK_NULL_HANDLE.
|
||||
//
|
||||
// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
|
||||
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
|
||||
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
||||
// The first seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array
|
||||
// with its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and
|
||||
// 1D ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
||||
// not merely survival: what a layer receives is a function of its own index, so an attachment that
|
||||
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
|
||||
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
|
||||
// never landed here" reads differently from "the wrong layer landed here".
|
||||
//
|
||||
// Case (8) is the same collapse one step downstream, and case (6) is what found it: the READBACK
|
||||
// of a cube map ignored the face it was asked for and answered +X for all six. Every case here
|
||||
// that reads a layered target back depends on the readback addressing the layer it names, so it
|
||||
// belongs beside them - and case (6) had to be written around it, which is the strongest argument
|
||||
// there is that it was never pinned.
|
||||
//
|
||||
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
|
||||
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
|
||||
@@ -96,6 +102,10 @@ namespace MGITest {
|
||||
// mismatch means a real miss rather than rounding.
|
||||
constexpr Rgba8 kClearColor{17, 68, 187, 255};
|
||||
|
||||
// The six cube faces in the order GL numbers them, which is also the order Vulkan keeps
|
||||
// them in as array layers (GL 4.6 core 8.5.3 / VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT).
|
||||
const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
||||
|
||||
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
|
||||
// says which layer it came from) and b names the PASS (so "the second draw was not
|
||||
// rejected" is distinguishable from "the first draw never happened").
|
||||
@@ -357,6 +367,27 @@ void main()
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A cube map whose six faces are UPLOADED with their own colours - the same
|
||||
// ExpectedColor(face, 0) the painted cube of case (8) ends up holding, so both can be
|
||||
// checked with one expectation. Uploaded rather than rendered means the CPU shadow and
|
||||
// the image agree, which is the premise the BY-NAME readback needs; see case (8).
|
||||
GLuint MakeFaceColoredCubeMap() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const std::vector<Rgba8> seed(static_cast<std::size_t>(kExtent) * kExtent,
|
||||
ExpectedColor(face, 0));
|
||||
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
|
||||
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
|
||||
// gives 1 however many layers there are.
|
||||
@@ -487,6 +518,34 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// Every texel of one cube FACE is that face's own colour. When it is not, the message
|
||||
// says whose colour answered instead - which is the whole point here: a readback that
|
||||
// ignores the face token does not return garbage, it returns another face's perfectly
|
||||
// plausible texels, and "+X's colour came back for -Y" is the sentence that names the
|
||||
// defect. `what` is the spelling under test, since three of them read the same faces.
|
||||
void ExpectFaceColor(const std::vector<Rgba8>& texels, int face, const char* what) {
|
||||
const Rgba8 expected = ExpectedColor(face, 0);
|
||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
||||
const Rgba8 actual = texels[i];
|
||||
if (actual == expected) continue;
|
||||
std::string blame;
|
||||
if (actual.r == kPoison && actual.g == kPoison) {
|
||||
blame = " - the poison, so nothing was ever written to this face";
|
||||
} else {
|
||||
for (int other = 0; other < 6; ++other) {
|
||||
if (other != face && actual == ExpectedColor(other, 0)) {
|
||||
blame = std::string(" - which is face ") + kFaceNames[other] + "'s colour";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ADD_FAILURE() << what << ": face " << kFaceNames[face] << " texel " << i << " is "
|
||||
<< Describe(actual) << ", expected " << Describe(expected) << blame;
|
||||
// One message per face is enough to say what happened.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
@@ -883,13 +942,16 @@ void main()
|
||||
|
||||
// Every face, read back through an FBO that names THAT face.
|
||||
//
|
||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
|
||||
// where only +X had been cleared, that spelling returned the cleared colour for all
|
||||
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
|
||||
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
|
||||
// else, and the pending clear is long gone by now (materialised and popped above), so
|
||||
// this readback cannot alter what it is measuring.
|
||||
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): when this case was written
|
||||
// that spelling could not see per-face state on DirectVulkan at all - measured against
|
||||
// a tree where only +X had been cleared it returned the cleared colour for all six
|
||||
// faces - so a case built on it would have been unfalsifiable. That is a readback
|
||||
// defect rather than an attachment one, and case (8) below is where it is pinned and
|
||||
// fixed; this case keeps the independent spelling deliberately, because it must go on
|
||||
// measuring the CLEAR whatever the readback does. glFramebufferTexture2D +
|
||||
// glReadPixels names one face and nothing else, and the pending clear is long gone by
|
||||
// now (materialised and popped above), so this readback cannot alter what it is
|
||||
// measuring.
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const GLuint faceFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
||||
@@ -958,5 +1020,114 @@ void main()
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (8) THE CUBE FACE TOKEN A READBACK IS GIVEN, AND WHETHER IT HONOURS IT.
|
||||
//
|
||||
// Case (6) above had to route around glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face)
|
||||
// entirely: measured against a tree where only the +X face had been cleared, that spelling
|
||||
// returned +X's colour for all six face tokens. This case is that observation turned into
|
||||
// an assertion, and it is about the READBACK, not the attachment.
|
||||
//
|
||||
// THE DEFECT. DirectVulkan's GetTextureImage derived its copy geometry from the IMAGE's
|
||||
// target alone. A plain GL_TEXTURE_CUBE_MAP is not one of the array targets, so the layer
|
||||
// count collapsed to one - correct, one face IS one layer - but nothing ever turned the
|
||||
// face the TARGET TOKEN named into the copy's baseArrayLayer, which stayed 0. All six face
|
||||
// tokens therefore read array layer 0 and answered +X: five of a cube map's six faces were
|
||||
// unreadable through the entry point GL provides for reading them. Nothing announces it -
|
||||
// the call succeeds, raises no error, and hands back entirely plausible texels from the
|
||||
// wrong face. The conversion it was missing already existed twice over, as the clear and
|
||||
// render-pass managers' ResolveAttachmentBaseArrayLayer.
|
||||
//
|
||||
// glGetTextureSubImage is the same question asked by name: GL 4.6 core 8.11.4 addresses a
|
||||
// cube map's faces through zoffset. That spelling was not merely reading the wrong face,
|
||||
// it could not read ANY face - measured pre-fix, all six returned INVALID_OPERATION on
|
||||
// both backends. Two independent reasons, and it took both to make even zoffset 0 fail:
|
||||
// the z range was measured against the level's z, which is one face's 1, so five of the
|
||||
// six looked like a partial read; and the destination-size check summed all six faces, so
|
||||
// the one face's worth of buffer a single-face read has any reason to pass was rejected as
|
||||
// too small.
|
||||
//
|
||||
// Each face is painted its OWN colour, so a collapse onto layer 0 does not merely read
|
||||
// "wrong": the failure names the face that answered. The cube is poisoned first and then
|
||||
// painted through the GPU, so an answer served from the stale CPU shadow is also called out
|
||||
// by name rather than passing. And the per-face FBO + glReadPixels read is the control: it
|
||||
// names one face and nothing else, so if IT disagrees the defect is in how the faces were
|
||||
// written and this case is measuring the wrong thing.
|
||||
//
|
||||
// DirectGLES attaches the named face to a scratch FBO and reads that, so it answers the
|
||||
// face token correctly throughout - a red there means this case is wrong. Its by-name
|
||||
// readback is a different matter and gets a texture of its own; see the third block.
|
||||
TEST_F(LayeredAttachmentShapeScenario, CubeMapFaceReadbackAnswersTheFaceItWasAskedFor) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint cube = MakePoisonedCubeMap();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
|
||||
|
||||
// Paint every face its own colour through an FBO that names that one face. A clear
|
||||
// rather than a draw, so nothing here depends on a shader stage being present.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
GLuint faceFbos[6] = {};
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
faceFbos[face] = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
|
||||
const Rgba8 want = ExpectedColor(face, 0);
|
||||
glClearColor(want.r / 255.0f, want.g / 255.0f, want.b / 255.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "painting the six faces errored";
|
||||
|
||||
// The control. If this is red, the faces do not hold six different values and the two
|
||||
// readbacks below are being measured against a premise that is not true.
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the control read of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "control: per-face FBO + glReadPixels");
|
||||
}
|
||||
|
||||
// The subject: the face TOKEN.
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glGetTexImage(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "glGetTexImage(GL_TEXTURE_CUBE_MAP_<face>)");
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
|
||||
// The same question by name, where zoffset is the face.
|
||||
//
|
||||
// On a cube map UPLOADED face by face rather than the painted one above, because the
|
||||
// by-name readback has no backend entry outside DirectVulkan and answers from the CPU
|
||||
// shadow there - a separate, pre-existing gap that has nothing to do with which face
|
||||
// gets read. Asking it about GPU-painted content would make this red on DirectGLES for
|
||||
// a reason the case is not about; asking it about uploaded content leaves exactly one
|
||||
// thing either backend can get wrong, which is the face. DirectVulkan still answers
|
||||
// this one out of the image, so the layer collapse is just as visible here.
|
||||
const GLuint uploaded = MakeFaceColoredCubeMap();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "uploading the six faces failed";
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glGetTextureSubImage(uploaded, 0, 0, 0, face, kExtent, kExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
static_cast<GLsizei>(texels.size() * sizeof(Rgba8)), texels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTextureSubImage of face " << kFaceNames[face] << " errored";
|
||||
ExpectFaceColor(texels, face, "glGetTextureSubImage(zoffset = face)");
|
||||
}
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,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,554 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GL_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
|
||||
// INACTIVE.
|
||||
//
|
||||
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
|
||||
// capture or no capture. The CTS leans its whole tessellation suite on that - the
|
||||
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
|
||||
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
|
||||
// tessellation tests are sized from the answer - so a backend that answers 0
|
||||
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
|
||||
//
|
||||
// DirectVulkan serves the query from the transform-feedback stream query's
|
||||
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
|
||||
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
|
||||
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
|
||||
// the pair reads back 0. Where the bring-up probe measures that defect with a
|
||||
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
|
||||
// renderer accumulates XFB-inactive draws through the best proven substitute
|
||||
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
|
||||
// rasterizer discard included), else pipeline statistics over clipping-stage
|
||||
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
|
||||
// answer, so on this machine they hold the reroute to the same numbers the
|
||||
// healthy stream path must produce - the "two pools must agree" assertion - and
|
||||
// on a healthy driver they pin the stream path itself.
|
||||
//
|
||||
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
|
||||
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
|
||||
// is a different mechanism with its own tests.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
|
||||
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
|
||||
// same program and simply never begin a span.
|
||||
const char* const kVertexSource = R"(#version 430 core
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vs_out_value = vec4(1.0);
|
||||
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
|
||||
// triangle per patch - the count the tessellation cases assert.
|
||||
const char* const kTessVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* const kTessControlSource = R"(#version 430 core
|
||||
layout(vertices = 1) out;
|
||||
void main() {
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
const char* const kTessEvalSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
void main() {
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The same tessellation pipeline with something to capture, so that
|
||||
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
|
||||
// open (but paused) capture span AND a tessellator in one program.
|
||||
const char* const kTessEvalCaptureSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
out vec4 te_out_value;
|
||||
void main() {
|
||||
te_out_value = vec4(1.0);
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != std::string("DirectVulkan")) {
|
||||
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
|
||||
<< Gl().BackendName()
|
||||
<< " answers this query from a different mechanism";
|
||||
}
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenQueries(2, m_queries);
|
||||
ASSERT_NE(m_queries[0], 0u);
|
||||
ASSERT_NE(m_queries[1], 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
|
||||
m_queries[0] = m_queries[1] = 0;
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// captureVarying: the name to record with glTransformFeedbackVaryings, or
|
||||
// nullptr for a program that can never open a capture span.
|
||||
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||
const char* captureVarying) {
|
||||
std::vector<GLuint> shaders;
|
||||
for (const auto& [type, source] : stages) {
|
||||
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||
if (shader == 0) {
|
||||
for (const GLuint built : shaders) glDeleteShader(built);
|
||||
return 0;
|
||||
}
|
||||
shaders.push_back(shader);
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (captureVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram() {
|
||||
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
|
||||
}
|
||||
|
||||
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTessGenLevel < 1) return 0;
|
||||
return BuildProgram(
|
||||
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER,
|
||||
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
|
||||
withCaptureVarying ? "te_out_value" : nullptr);
|
||||
}
|
||||
|
||||
// A capture span that is open but PAUSED. The pause closes the capture, so
|
||||
// every draw inside it is XFB-inactive at the backend - the stream query's
|
||||
// silent case - while the GL span stays active. `program` must be the one
|
||||
// that is bound: GL requires the same program at resume.
|
||||
void BeginPausedSpan() {
|
||||
glGenBuffers(1, &m_captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glPauseTransformFeedback();
|
||||
}
|
||||
|
||||
void EndPausedSpan() {
|
||||
glResumeTransformFeedback();
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
|
||||
m_captureBuffer = 0;
|
||||
}
|
||||
|
||||
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||
GLuint QueryGenerated(const std::function<void()>& record) {
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
||||
record();
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint generated = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
static GLenum DrainGLErrors() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||
GLuint m_captureBuffer = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// The plain shape: no capture object was ever bound, no span begun, no
|
||||
// rasterizer discard - just a GENERATED query around two triangles. On a
|
||||
// healthy driver the stream query answers it; on an affected one the armed
|
||||
// reroute must produce the same 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
|
||||
"inactive (GL 4.6 core 13.4)";
|
||||
}
|
||||
|
||||
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
|
||||
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
|
||||
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
|
||||
// tests' capture buffers on the affected device.
|
||||
//
|
||||
// On lavapipe this case holds through the dedicated
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
|
||||
// what makes a discarded draw countable there - llvmpipe's clipping
|
||||
// statistics AND stream query both read 0 under discard).
|
||||
//
|
||||
// The value-conditioned skip below is deliberate and narrow, for a stack
|
||||
// with NO counter that survives discard: there this case is unfalsifiable,
|
||||
// and a red would indict MobileGL for a hole the bring-up probe already
|
||||
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
|
||||
// exact-zero answer IS the capability signal - any wrong nonzero count
|
||||
// still fails - and on every driver that counts discarded draws at all the
|
||||
// full assertion runs. The device probe list holds this shape on the Mali.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (generated == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
|
||||
"primitives-generated query, clipping statistics) survives "
|
||||
"rasterizer discard for an XFB-inactive draw on this stack - the "
|
||||
"shape is unfalsifiable here; the bring-up probe measures the same "
|
||||
"hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "rasterizer discard drops primitives after clipping and must not hide them from "
|
||||
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
|
||||
"tessellator with";
|
||||
}
|
||||
|
||||
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
|
||||
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
|
||||
// protocol derives everything from. Undiscarded, so that the answer is
|
||||
// holdable on this machine through whichever accounting path is armed (the
|
||||
// discard interaction is the case above's business, measured separately).
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram();
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u)
|
||||
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
|
||||
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
|
||||
}
|
||||
|
||||
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
|
||||
// captured one, then another XFB-inactive one. The GENERATED answer must
|
||||
// accumulate across the two accounting paths the armed reroute splits them
|
||||
// into (stream slots for the captured draw, statistics slots for the
|
||||
// others), and WRITTEN must stay exactly the captured draw's count - the
|
||||
// pairing the stream path exists to keep exact. Undiscarded, so the
|
||||
// accumulation invariant is holdable on this machine (see the discard
|
||||
// case's comment); the triangles rasterize into the harness framebuffer,
|
||||
// which nothing here reads.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
||||
const GLuint generated = QueryGenerated([]() {
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
|
||||
glEndTransformFeedback();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
|
||||
});
|
||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||
|
||||
GLuint written = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
|
||||
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
||||
}
|
||||
|
||||
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
|
||||
//
|
||||
// glPauseTransformFeedback closes the capture without closing the span, so a
|
||||
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
|
||||
// exactly as silent for it as for a draw with no span at all - while
|
||||
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
|
||||
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
|
||||
// silences). The frontend does keep a CPU counter for paused draws, but it can
|
||||
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
|
||||
// these draws are the reroute's business like any other - and the trap on the
|
||||
// other side is counting them TWICE, once in each accounting.
|
||||
//
|
||||
// Each case measures the SAME draw twice: once with no span open at all (the
|
||||
// capability control - what this stack can count) and once inside the paused
|
||||
// span, and requires the two to agree. That differential is what makes these
|
||||
// cases falsifying rather than vacuous: a stack where no counter reaches a
|
||||
// capture-less draw fails the control and skips, while a stack that counts the
|
||||
// unpaused draw and answers 0 for the paused one - which is what excluding
|
||||
// paused draws from the reroute produced - fails, instead of skipping into
|
||||
// green.
|
||||
|
||||
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
|
||||
// CPU delta, this reads 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
|
||||
"draws are CPU-counted on top of whatever the stream query says";
|
||||
}
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half of the comparison proves nothing; the "
|
||||
"bring-up probe measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "one triangle drawn while the capture span is paused is still one primitive "
|
||||
"generated - counted once, by whichever accounting owns it, never by two of them "
|
||||
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
|
||||
}
|
||||
|
||||
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
|
||||
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
|
||||
// the CTS's tessellator-measuring shape. Excluding paused draws from the
|
||||
// reroute left this counted by nothing at all on the affected device.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
|
||||
"on this stack, so the paused half proves nothing; the bring-up probe "
|
||||
"measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u)
|
||||
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
|
||||
"exactly one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "pausing the capture does not stop the tessellator from generating that triangle, "
|
||||
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
|
||||
"patch draw left out of the reroute is counted by nothing at all";
|
||||
}
|
||||
|
||||
// The other half of the same hole: the instanced entry points never reach the
|
||||
// frontend's paused accounting either, so a paused instanced draw excluded from
|
||||
// the reroute is likewise counted by nothing.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused =
|
||||
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half proves nothing";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "four instances generate four primitives whether or not the capture span is "
|
||||
"paused, and no instanced entry point reaches the frontend's paused accounting";
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||
// is green here whether the reroute ran or not (that is the "two pools
|
||||
// agree" point), so none of them can say the pinned lane actually exercised
|
||||
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
|
||||
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
|
||||
// XFB-inactive draw inside a GENERATED span must make the renderer say -
|
||||
// through its latched MGLOG_I - that it engaged the reroute. It reads
|
||||
// MG_Config not at all (on Android this module links the shipping library)
|
||||
// and trusts only the log bytes appended after it started.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
|
||||
"is what the PrimGenReroute. ctest entry does with "
|
||||
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
|
||||
"decides and this machine's verdict is its own business";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it rerouted anything; the PrimGenReroute. ctest "
|
||||
"entry sets both";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
// Taken BEFORE the draw, so the line this looks for can only be one this
|
||||
// process wrote for this span. The latch fires on the FIRST rerouted
|
||||
// draw, which is inside the query below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
|
||||
"a GENERATED query, and the renderer never reported engaging the reroute. The "
|
||||
"quirk is not armed - check the override mapping "
|
||||
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
|
||||
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,178 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
|
||||
//
|
||||
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
|
||||
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
|
||||
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
|
||||
// glSampleMaski last wrote.
|
||||
//
|
||||
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
|
||||
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
|
||||
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
|
||||
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
|
||||
// a fully black draw:
|
||||
//
|
||||
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
|
||||
// ... render ...
|
||||
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
|
||||
//
|
||||
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
|
||||
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
|
||||
// shape every application uses.
|
||||
//
|
||||
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
|
||||
// one asserts that the mask changed nothing.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kGreenFragmentSource = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampleMaskScopeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
// Process-wide GL state: leaving it set would hand the next scenario in this
|
||||
// process the very bug under test.
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
void ExpectQuadStillPaints(const char* what) {
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
|
||||
|
||||
const Image image = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
|
||||
<< what << ": an all-black target means the sample mask discarded every fragment, "
|
||||
<< "which GL says it cannot do on a single-sample framebuffer";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
|
||||
// masked off if the mask is applied at all.
|
||||
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
|
||||
}
|
||||
|
||||
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
|
||||
// set.
|
||||
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
|
||||
}
|
||||
|
||||
// Control: the same mask word with the capability disabled has never had any effect, so this
|
||||
// one passed before the fix too. It is here so a regression that ignores the enable bit
|
||||
// instead of the sample count is still caught.
|
||||
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glDisable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0x0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
|
||||
}
|
||||
|
||||
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
|
||||
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
|
||||
// mask change, both to the same single-sample target, both required to paint.
|
||||
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
glEnable(GL_SAMPLE_MASK);
|
||||
glSampleMaski(0, 0xFFFFFFFFu);
|
||||
ExpectQuadStillPaints("first draw, full mask");
|
||||
glSampleMaski(0, 0x2);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
|
||||
ExpectQuadStillPaints("second draw, mask 0x2");
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
|
||||
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
|
||||
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
|
||||
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
|
||||
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
GLint words = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
|
||||
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
|
||||
for (GLint word = 0; word < words; ++word) {
|
||||
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
|
||||
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,231 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampledSetStalenessScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A TEXTURE THAT BECOMES COMPLETE WITHOUT A REBIND MUST RE-ENTER THE SAMPLED SET.
|
||||
//
|
||||
// DirectVulkan does not bind a texture GL calls incomplete: it substitutes a fallback so the
|
||||
// sampler reads (0,0,0,1) instead of losing the draw. That decision is made twice per draw - once
|
||||
// by CollectSampledTextures, which builds the list SetupDraw syncs, materialises pending clears
|
||||
// for and transitions to a sampled layout BEFORE the render pass opens, and once by the descriptor
|
||||
// resolve inside the pass. Both ask SamplesAsIncompleteTexture.
|
||||
//
|
||||
// The per-draw memo that lets the first of those be skipped was keyed only on the program, the
|
||||
// transform flags and the texture BIND generation. Completeness is not a function of any of them:
|
||||
// it moves on a filter change (glTexParameteri / glSamplerParameteri), on a level-range change,
|
||||
// and on an upload that fills the mip chain - none of which bind anything. So a texture that went
|
||||
// incomplete -> complete under a fixed binding kept being answered out of the memo as "not in the
|
||||
// set", and the work SetupDraw does for the set never happened for it:
|
||||
//
|
||||
// * its queued clear was never materialised, so the draw sampled pre-clear content - wrong
|
||||
// pixels, no validation layer needed, which is what the case below detects; and
|
||||
// * its layout transition moved into the descriptor resolve, which records
|
||||
// vkCmdPipelineBarrier inside an already-open render pass whose subpass declares no
|
||||
// self-dependency - the exact hazard CollectSampledTextures exists to prevent.
|
||||
//
|
||||
// The fix adds the sampling-resolution generation to that memo key, which is the counter the
|
||||
// codebase already maintains for "what a unit resolves to changed without a bind" and which both
|
||||
// TextureObjectBase::BumpShapeVersion and SamplerObject::BumpVersion move.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kFboSize = 32;
|
||||
constexpr int kTexSize = 8;
|
||||
|
||||
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// texelFetch, not texture(): the point is WHICH image is sampled, and a fetch cannot be
|
||||
// explained away by filtering.
|
||||
constexpr const char* kSampleFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_tex;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = texelFetch(u_tex, ivec2(0, 0), 0);
|
||||
}
|
||||
)";
|
||||
|
||||
class SampledSetStalenessScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
m_target = MakeColorFbo(kFboSize, kFboSize);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
// The sampled texture: ONE level, and no glTexParameteri at all, so MIN_FILTER
|
||||
// keeps its initial GL_NEAREST_MIPMAP_LINEAR and GL calls it mipmap-incomplete.
|
||||
glGenTextures(1, &m_texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
std::vector<unsigned char> green(static_cast<std::size_t>(kTexSize * kTexSize * 4), 0);
|
||||
for (std::size_t i = 0; i < green.size(); i += 4) {
|
||||
green[i + 1] = 255;
|
||||
green[i + 3] = 255;
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
green.data());
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// One draw of the fullscreen quad sampling texel (0,0) of whatever unit 0 holds, and
|
||||
// NO readback. That matters: a readback submits and waits, which ends the command
|
||||
// buffer and resets the per-draw memos with it - so a case that read back between its
|
||||
// two draws would never leave a stale entry to catch. The two draws here have to land
|
||||
// in one recording.
|
||||
void DrawOnly() {
|
||||
BindFbo(m_target);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(m_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const GLint location = glGetUniformLocation(m_program, "u_tex");
|
||||
if (location != -1) glUniform1i(location, 0);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw raised a GL error";
|
||||
}
|
||||
|
||||
ColorFbo m_target{};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_texture = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The full sequence, ordered so the ONLY state change between the two draws is the filter.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenAFilterChangeCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// 1. Queue a clear on the texture through an FBO and take it straight back out, with no
|
||||
// draw in between - the "attach -> clear -> detach" shape that leaves the clear
|
||||
// pending for whoever samples the texture next.
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
|
||||
// 2. Draw while the texture is still incomplete. The backend substitutes its fallback,
|
||||
// and the per-draw memo records the resulting sampled set.
|
||||
DrawOnly();
|
||||
|
||||
// 3. Make it complete. No bind, no upload, no program change - one filter write, which is
|
||||
// exactly the state the old memo key could not see.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the filter raised a GL error";
|
||||
|
||||
// 4. Draw again, into the same recording, and only now read back. The texture is in the
|
||||
// sampled set now, so its queued clear has to be materialised before the pass opens and
|
||||
// the fetch has to see RED. Reading the green the texture was uploaded with means the
|
||||
// clear was never materialised, i.e. the texture never entered the set - the stale-memo
|
||||
// bug. Black means the fallback was still being handed out.
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the completeness flip"))
|
||||
<< "green means the queued clear was never materialised, so the texture never re-entered "
|
||||
"the sampled set after the filter change; blue means the draw did not happen at all";
|
||||
}
|
||||
|
||||
// The same flip driven from a SAMPLER OBJECT rather than the texture's own parameters. It is
|
||||
// the other half of what feeds the completeness predicate, it moves the same generation, and
|
||||
// it likewise binds nothing.
|
||||
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenASamplerObjectCompletesTheTexture) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLuint sampler = 0;
|
||||
glGenSamplers(1, &sampler);
|
||||
// Bound BEFORE the first draw, still carrying the mipmapping default, so binding it is
|
||||
// not what changes between the two draws.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glBindSampler(0, sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object raised a GL error";
|
||||
|
||||
GLuint clearFbo = 0;
|
||||
glGenFramebuffers(1, &clearFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, red);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &clearFbo);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
|
||||
|
||||
BindFbo(m_target);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawOnly();
|
||||
|
||||
// One parameter write on an ALREADY-BOUND sampler object.
|
||||
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "changing the sampler filter raised a GL error";
|
||||
|
||||
DrawOnly();
|
||||
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
|
||||
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
|
||||
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
|
||||
"the draw after the sampler-object flip"))
|
||||
<< "green means the queued clear was never materialised after the sampler parameter change";
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &sampler);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,896 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
|
||||
//
|
||||
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
|
||||
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
|
||||
// case here is the reduced form of a conformance body that fails on a device:
|
||||
//
|
||||
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
|
||||
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
|
||||
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
|
||||
// two backends reach it by completely different routes - DirectGLES has to name a
|
||||
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
|
||||
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
|
||||
//
|
||||
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
|
||||
// user-declared per-vertex interface block, both read back out of gl_in[] by the
|
||||
// evaluation stage and only then captured. This is the shape of
|
||||
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
|
||||
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
|
||||
// the family still fails: gl_Position arrives, and everything travelling beside it in
|
||||
// the same patch does not.
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
|
||||
// absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure instead of looking like an ordinary numeric mismatch.
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class TessellationXfbCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static GLint MaxPatchVertices() {
|
||||
GLint value = 0;
|
||||
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
|
||||
DrainErrors();
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over a single patch. Returns the capture buffer read back as
|
||||
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
|
||||
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
|
||||
float expected, float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture never reached "
|
||||
<< "these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// Defined below the shader builders it uses. `withPointSize` is the conformance
|
||||
// body's own should_pass_pointsize_data axis.
|
||||
void RunPerVertexPayloadCase(bool withPointSize);
|
||||
|
||||
// Why the gl_PointSize cases cannot be run here, or empty when they can.
|
||||
//
|
||||
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
|
||||
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
|
||||
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
|
||||
// has no query that reports either, so this probes for it by running a program.
|
||||
//
|
||||
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
|
||||
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
|
||||
// compares that against the identical program without the write. A backend that
|
||||
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
|
||||
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
|
||||
// comes back untouched too - which is a capability answer, not a capture answer. If
|
||||
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
|
||||
// the cases run and FAIL rather than skipping on an unrelated breakage.
|
||||
//
|
||||
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
|
||||
// a `return`, so a void helper would leave only the helper and let the case run its
|
||||
// assertions anyway and report Failed instead of Skipped.
|
||||
std::string WhyPointSizeCasesCannotRun();
|
||||
|
||||
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
|
||||
// extensions are independent (Loader models them as two PointSizeTier fields fed by
|
||||
// four distinct strings, and neither implies the other), so a driver with
|
||||
// tessellation point size and no geometry point size passes the probe above and
|
||||
// still cannot run the case below. Same two-program shape, one stage over.
|
||||
//
|
||||
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
|
||||
// a hardcoded frontend constant (256) with no capability behind it, so "does this
|
||||
// stack have a geometry stage at all" can only be answered by trying to build one -
|
||||
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
|
||||
// reason.
|
||||
std::string WhyGeometryPointSizeCaseCannotRun();
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Built-ins captured BY NAME from the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
const char* const kMinimalVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kMinimalTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Values no stale buffer would hold by accident. The two sources differ ONLY by
|
||||
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
|
||||
// built-in is not even declared in a tessellation stage without
|
||||
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
|
||||
const char* const kPositionTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
||||
gl_PointSize = 5.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// The two probe programs. They differ by one statement; both capture `probe_value`,
|
||||
// which has nothing to do with point size.
|
||||
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 3.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
out float probe_value;
|
||||
void main()
|
||||
{
|
||||
probe_value = 42.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const auto probeCaptures = [&](const char* tessEvalSource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"probe_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
|
||||
DrainErrors();
|
||||
return captured[0] == 42.0f;
|
||||
};
|
||||
|
||||
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
|
||||
if (withPointSize) return {};
|
||||
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
|
||||
// The control failed too, so nothing here is about point size.
|
||||
return {};
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
|
||||
"program captures an ordinary varying with the gl_PointSize write removed and captures "
|
||||
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
|
||||
"Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
// point_mode with every level at 1 emits three points, all carrying the same
|
||||
// constant; only the first record has to be right for the mechanism to be proven.
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The per-vertex payload the control stage hands to the evaluation stage.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body's own shapes, reduced to one patch and parameterised by the
|
||||
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
|
||||
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
|
||||
// which it varies together with point_mode - and which decides whether the whole
|
||||
// program even involves the per-vertex built-in that ESSL gates behind an extension.
|
||||
std::string PayloadVertexSource(bool withPointSize) {
|
||||
return R"(#version 420 core
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
|
||||
R"(};
|
||||
void main()
|
||||
{
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(vertices = )" + std::to_string(outputVertices) +
|
||||
R"() out;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_out[];
|
||||
out OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} result[];
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize
|
||||
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
|
||||
: "") +
|
||||
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
|
||||
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
|
||||
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
|
||||
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
|
||||
gl_InvocationID + 3, gl_InvocationID + 4);
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
|
||||
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
|
||||
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
|
||||
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
|
||||
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
||||
return R"(#version 420 core
|
||||
layout(isolines, equal_spacing, ccw, point_mode) in;
|
||||
in gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(} gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
)" + perVertexTail +
|
||||
R"(};
|
||||
in OUT_TC
|
||||
{
|
||||
vec2 value1;
|
||||
ivec4 value2;
|
||||
} tc_data[];
|
||||
|
||||
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
|
||||
R"(out vec4 te_position;
|
||||
out vec2 te_value1;
|
||||
out flat ivec4 te_value2;
|
||||
|
||||
void main()
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
|
||||
R"( te_position = vec4 (0.0);
|
||||
te_value1 = vec2 (0.0);
|
||||
te_value2 = ivec4(0);
|
||||
|
||||
for (int n = 0; n < )" + std::to_string(inputVertices) +
|
||||
R"(; ++n)
|
||||
{
|
||||
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
|
||||
R"( te_position += gl_in [n].gl_Position;
|
||||
te_value1 += tc_data[n].value1;
|
||||
te_value2 += tc_data[n].value2;
|
||||
}
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
// The reduced conformance body. `withPointSize` selects between its two halves;
|
||||
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
|
||||
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
|
||||
// evaluation stage - is the same on both.
|
||||
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
if (withPointSize) {
|
||||
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
||||
}
|
||||
const GLint patchVertices = MaxPatchVertices();
|
||||
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
|
||||
|
||||
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
|
||||
// the control stage runs that many invocations and every one of them contributes.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
|
||||
if (withPointSize) varyings.push_back("te_pointsize");
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
|
||||
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
|
||||
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
varyings);
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
float referencePointSize = 0.0f;
|
||||
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
float referenceValue1[2] = {0.0f, 0.0f};
|
||||
int referenceValue2[4] = {0, 0, 0, 0};
|
||||
for (int n = 0; n < patchVertices; ++n) {
|
||||
referencePointSize += 1.0f / static_cast<float>(n + 1);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
referencePosition[c] += static_cast<float>(n * 4 + c);
|
||||
referenceValue2[c] += n + 1 + c;
|
||||
}
|
||||
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
|
||||
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
|
||||
}
|
||||
|
||||
// isolines with every level at 1 emits two points; the record stride is
|
||||
// vec4 + vec2 + ivec4 [+ float] components.
|
||||
const std::size_t stride = withPointSize ? 11 : 10;
|
||||
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
|
||||
<< "te_position." << c << " (gl_in[].gl_Position)";
|
||||
}
|
||||
for (int c = 0; c < 2; ++c) {
|
||||
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
|
||||
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
|
||||
}
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const std::size_t index = static_cast<std::size_t>(6 + c);
|
||||
ASSERT_LT(index, captured.size());
|
||||
int actual = 0;
|
||||
std::memcpy(&actual, &captured[index], sizeof(actual));
|
||||
EXPECT_EQ(actual, referenceValue2[c])
|
||||
<< "te_value2." << c << " (the user per-vertex block's integer member)";
|
||||
}
|
||||
if (withPointSize) {
|
||||
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
|
||||
<< "te_pointsize (gl_in[].gl_PointSize)";
|
||||
}
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
|
||||
RunPerVertexPayloadCase(false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same built-in, one stage over.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
|
||||
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
|
||||
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
|
||||
// extensions that do not imply each other, so the geometry arm is a second code path
|
||||
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
|
||||
// shader, so without this case the arm ships untested.
|
||||
const char* const kPointSizeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = 4.0;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
// The control: identical but for the gl_PointSize write, so the pair answers "can this
|
||||
// stack host a geometry stage that names the built-in" without asking anything about
|
||||
// capture.
|
||||
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float gs_value;
|
||||
void main()
|
||||
{
|
||||
gs_value = 7.0;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
}
|
||||
)";
|
||||
|
||||
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
|
||||
const auto probeCaptures = [&](const char* geometrySource) {
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, geometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value"});
|
||||
if (program == 0) return false;
|
||||
const std::vector<float> poison(1, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
|
||||
GL_STATIC_DRAW);
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
float captured = kPoison;
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
|
||||
&captured);
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
DrainErrors();
|
||||
return captured == 7.0f;
|
||||
};
|
||||
|
||||
if (probeCaptures(kPointSizeGeometrySource)) return {};
|
||||
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
|
||||
// The control failed too, so this stack cannot run a capturing geometry stage at
|
||||
// all - which is not what this case is about, and is the question the dead
|
||||
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
|
||||
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
|
||||
// and goes red on its own if that is what actually broke.
|
||||
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
|
||||
}
|
||||
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
|
||||
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
|
||||
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
|
||||
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
|
||||
}
|
||||
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
|
||||
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gs_value", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
const std::vector<float> poison(2, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(2, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
|
||||
"the stage carrying it fails to compile";
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The conformance body's own READBACK, which is not glGetBufferSubData.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Every case above reads the capture back with glGetBufferSubData because that is the
|
||||
// shortest path to the bytes. The conformance bodies do something else: they respecify
|
||||
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
|
||||
// while it is simultaneously bound to indexed capture point 0, and then read it with
|
||||
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
|
||||
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
|
||||
// call, so the sequence itself is worth pinning: none of the map path's error conditions
|
||||
// may fire, and the mapped bytes must be the captured ones.
|
||||
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_Position"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
|
||||
for (int iteration = 0; iteration < 2; ++iteration) {
|
||||
// Respecified through the generic binding, exactly as the conformance body does,
|
||||
// while the same buffer is still bound to capture point 0.
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
|
||||
|
||||
const auto* mapped =
|
||||
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
|
||||
GL_MAP_READ_BIT));
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
|
||||
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
|
||||
const std::vector<float> captured(mapped, mapped + kFloats);
|
||||
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
|
||||
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
|
||||
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
|
||||
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
|
||||
// without asking for it does not merely lose the value - the stage fails to compile
|
||||
// and the whole program is replaced by program 0.
|
||||
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
|
||||
RunPerVertexPayloadCase(true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The same capture through a PROGRAM PIPELINE OBJECT.
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// The conformance body runs each of its configurations twice: once with a monolithic
|
||||
// program object and once with a pipeline of four separable programs, the capture
|
||||
// declared on the separable EVALUATION program. That second shape goes through the
|
||||
// hidden composite the pipeline object builds for the draw, and it is the only place a
|
||||
// tessellation capture and the composite meet - so the capture list has to survive being
|
||||
// taken from a program that is not the one bound.
|
||||
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
// One separable program per stage. Only the evaluation program carries the capture
|
||||
// list, because it is the one whose outputs are captured.
|
||||
const auto buildSeparable = [&](GLenum stage, const char* source,
|
||||
const std::vector<const char*>& varyings) -> GLuint {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
glAttachShader(program, shader);
|
||||
if (!varyings.empty()) {
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(shader);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
};
|
||||
|
||||
m_buildLog.clear();
|
||||
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
|
||||
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
|
||||
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
|
||||
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
|
||||
const GLuint evalProgram =
|
||||
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
|
||||
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
|
||||
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
|
||||
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
|
||||
|
||||
GLuint pipeline = 0;
|
||||
glGenProgramPipelines(1, &pipeline);
|
||||
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
|
||||
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
|
||||
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
|
||||
|
||||
constexpr std::size_t kFloats = 4 * 3;
|
||||
const std::vector<float> poison(kFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_DRAW);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(0);
|
||||
glBindProgramPipeline(pipeline);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> captured(kFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
||||
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glBindProgramPipeline(0);
|
||||
glDeleteProgramPipelines(1, &pipeline);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -119,6 +119,73 @@ void main() {
|
||||
imageStore(u_unbound, int(index), uvec4(7u));
|
||||
g_data[index] = index + 1u;
|
||||
}
|
||||
)";
|
||||
|
||||
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
|
||||
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
|
||||
// level and no mip chain - GL calls the second one incomplete for the initial
|
||||
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
|
||||
// texture the backend then fails to back.
|
||||
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
|
||||
uniform sampler2D u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texture(u_unbound, vec2(0.0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
|
||||
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
|
||||
// unused one at an empty unit, so whichever of the two is unused has to have a
|
||||
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
|
||||
// cannot stand in for it.
|
||||
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform sampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = texelFetch(u_unbound, ivec2(0), 0);
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
||||
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
||||
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
||||
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
||||
// been created.
|
||||
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform usampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
||||
uniform isampler2DMS u_unbound;
|
||||
uniform int u_readUnbound;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
if (u_readUnbound != 0) {
|
||||
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
||||
}
|
||||
o_color = color;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
||||
@@ -369,6 +436,65 @@ void main() {
|
||||
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
||||
}
|
||||
|
||||
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
|
||||
}
|
||||
|
||||
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
|
||||
// unit whose DEFAULT texture object has an image but no mip chain.
|
||||
//
|
||||
// DirectVulkan resolved such a binding twice, through two different predicates that
|
||||
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
|
||||
// pre-syncs and transitions every texture the draw will sample, asked only whether the
|
||||
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
|
||||
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
|
||||
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
|
||||
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
|
||||
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
|
||||
// a SIGSEGV inside the draw, not a degraded picture.
|
||||
//
|
||||
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
|
||||
// texture a base level, so the FIRST case in a process survived and every later one with an
|
||||
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
|
||||
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
|
||||
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
|
||||
// object bound there a FORMAT and a zero-sized level - which is what a bare
|
||||
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
|
||||
// state reset issues for every texture target. That combination is the whole point:
|
||||
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
|
||||
// and the texture stays in the sampled set;
|
||||
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
|
||||
// true and the descriptor holds the fallback instead;
|
||||
// * and it has no valid mip level, so the sync declines and hands back nullptr.
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
|
||||
|
||||
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
||||
}
|
||||
|
||||
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
|
||||
// QUALIFIER REMOVED.
|
||||
//
|
||||
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
|
||||
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
|
||||
// stages compile, the program links with an empty info log, the draw runs, and the consuming
|
||||
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
|
||||
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
|
||||
// from the blocks (and changing nothing else) makes the same program carry its payload. The
|
||||
// locations are not the application's in the first place: these shaders declare none, and
|
||||
// glslang's cross-stage IO resolver invents them.
|
||||
//
|
||||
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
|
||||
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
|
||||
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
|
||||
// location must still deliver its payload end to end. It is the assertion the affected device
|
||||
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
|
||||
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
|
||||
// cannot reproduce the defect at all.
|
||||
//
|
||||
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
|
||||
// entry, because llvmpipe carries a located block correctly and the driver POST would
|
||||
// therefore never turn the emulation on here. The SAME cases also run under the ambient
|
||||
// registrations with the emulation off, so both spellings of the interface are covered and a
|
||||
// regression in either shows up.
|
||||
//
|
||||
// Colour code, so a failure names its own cause:
|
||||
// green - the payload crossed all four stage boundaries, which is the pass.
|
||||
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
||||
// backend program was rejected and every draw became a no-op).
|
||||
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
||||
// failure is not about interface blocks.
|
||||
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
||||
// zeroed. That is what an interface whose two ends stopped matching looks like.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// NOTHING in these five stages declares a location. Every location the emitted ESSL
|
||||
// carries is invented by the cross-stage resolver, which is exactly the shape the
|
||||
// affected driver mishandles and exactly what the strip removes.
|
||||
//
|
||||
// Two members per block, of different types, because an interface that is matched by
|
||||
// name and member sequence rather than by location has to agree on the sequence too -
|
||||
// a repair that silently reordered or dropped a member would still light up green with
|
||||
// one member in the block.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
out VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} vs_out;
|
||||
out float vs_tcs_alive;
|
||||
void main()
|
||||
{
|
||||
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
vs_out.tint = vec2(0.25, 0.5);
|
||||
vs_tcs_alive = 1.0;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
in VsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_in[];
|
||||
in float vs_tcs_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tcs_out[];
|
||||
out float tcs_tes_alive[];
|
||||
void main()
|
||||
{
|
||||
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
||||
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
|
||||
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
// Distinct block names, so this case is about the LOCATION and nothing else; the
|
||||
// one-name-in-both-directions shape is the case below.
|
||||
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// The 420pack shape: ONE name for the block this stage consumes and the block it
|
||||
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
|
||||
// the rename gives the two blocks one spelling per producing stage, the strip takes
|
||||
// their locations off, and the interfaces still have to meet.
|
||||
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_in[];
|
||||
in float tcs_tes_alive[];
|
||||
out TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} tes_out;
|
||||
out float tes_gs_alive;
|
||||
void main()
|
||||
{
|
||||
tes_out.payload = tes_in[0].payload;
|
||||
tes_out.tint = tes_in[0].tint;
|
||||
tes_gs_alive = tcs_tes_alive[0];
|
||||
}
|
||||
)";
|
||||
|
||||
// One geometry source per evaluation stage, because the block it consumes is named
|
||||
// after the block the evaluation stage produced.
|
||||
const char* const kDistinctGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TesData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kCollidingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
in TcsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_in[];
|
||||
in float tes_gs_alive[];
|
||||
out GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} gs_out;
|
||||
out float gs_fs_alive;
|
||||
void EmitCorner(vec2 corner)
|
||||
{
|
||||
gs_out.payload = gs_in[0].payload;
|
||||
gs_out.tint = gs_in[0].tint;
|
||||
gs_fs_alive = tes_gs_alive[0];
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
EmitVertex();
|
||||
}
|
||||
void main()
|
||||
{
|
||||
EmitCorner(vec2(-1.0, -1.0));
|
||||
EmitCorner(vec2(-1.0, 1.0));
|
||||
EmitCorner(vec2( 1.0, -1.0));
|
||||
EmitCorner(vec2( 1.0, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// Green ONLY when both block members arrived: a repair that kept the first member and
|
||||
// lost the second would otherwise pass. Red when the plain varying is missing too, so
|
||||
// "the pipeline is broken" and "the block is broken" cannot be confused.
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
in GsData {
|
||||
vec4 payload;
|
||||
vec2 tint;
|
||||
} fs_in;
|
||||
in float gs_fs_alive;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
if (gs_fs_alive <= 0.5) {
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
|
||||
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
} else {
|
||||
fragColor = fs_in.payload;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
class UnlocatedIoBlockScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
if (!BackendHostsTessellationAndGeometry()) {
|
||||
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
||||
<< "carry a block through";
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
|
||||
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
|
||||
// frontend constants, so a stack with no five-stage pipeline is recognised by
|
||||
// trying to build one, not by asking.
|
||||
static bool BackendHostsTessellationAndGeometry() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
||||
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
||||
GL_FRAGMENT_SHADER};
|
||||
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
||||
geometrySource, kFragmentSource};
|
||||
|
||||
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
||||
bool ok = true;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
shaders[i] = glCreateShader(stages[i]);
|
||||
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
||||
glCompileShader(shaders[i]);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = InfoLog(shaders[i], true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) {
|
||||
if (shader != 0) glDeleteShader(shader);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (!linked) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
||||
// than something that could be mistaken for a zeroed payload.
|
||||
Rgba8 DrawAndReadCentre(GLuint program) const {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
glDrawArrays(GL_PATCHES, 0, 1);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
return pixel;
|
||||
}
|
||||
|
||||
static bool IsGreen(const Rgba8& pixel) {
|
||||
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// The library log this process is writing, or an empty path when none was
|
||||
// configured. MOBILEGL_LOG_FILE_PATH is read at log-init, before anything this
|
||||
// fixture can reach, so the ctest entry sets it and this only reads it back.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// How many bytes the library log already holds. Everything this fixture asserts on
|
||||
// is searched from here forward, because the file is APPENDED to by every process
|
||||
// in the lane and a line left behind by an earlier one would otherwise satisfy the
|
||||
// assertion without this process having done anything at all.
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static GLenum FirstGLError() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
}
|
||||
return std::string(log.data());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
|
||||
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
|
||||
"member arrived wrong, i.e. the interface stopped matching)";
|
||||
}
|
||||
|
||||
// The two repairs together. The rename is what makes the evaluation stage's two
|
||||
// TcsData blocks one spelling per producing stage; the strip then takes the locations
|
||||
// off the names the rename just settled. Either one alone leaves a working program on
|
||||
// these machines, so this case is here to catch the two of them disagreeing.
|
||||
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||
<< BuildLog();
|
||||
}
|
||||
|
||||
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "an interface block name reused across the two directions of one stage is legal "
|
||||
"desktop GLSL, but the program did not build: "
|
||||
<< BuildLog();
|
||||
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre))
|
||||
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REPAIR SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above renders green on llvmpipe whether the blocks were stripped or not -
|
||||
// this machine carries a located block correctly - so those cases pin that the strip
|
||||
// does no HARM and can say nothing about whether it happened. That leaves the arming
|
||||
// itself untested, and the arming is where the cheap mistake lives: Loader.cpp maps
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS onto the capability INVERTED (forcing the
|
||||
// emulation on means declaring located blocks UNSUPPORTED), and a one-line swap of
|
||||
// those two arms would disable the device repair with every test here still green.
|
||||
//
|
||||
// So this case asserts a LIBRARY OBSERVABLE against the environment, the shape
|
||||
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration uses: the environment
|
||||
// says the emulation is pinned on, therefore the library must SAY it stripped
|
||||
// something. The observable is the latched MGLOG_I DirectGLES emits the first time the
|
||||
// pass fires (Managers.cpp); it is INFO rather than DEBUG precisely so that this
|
||||
// assertion is possible in the builds CI runs.
|
||||
//
|
||||
// Two things it deliberately does NOT do: it does not read MG_Config (on Android this
|
||||
// module links the shipping library, which exports nothing internal - the reason
|
||||
// ViewportArrayScenario's control moved to the environment), and it does not trust the
|
||||
// whole log file, only the bytes appended after this test started.
|
||||
TEST_F(UnlocatedIoBlockScenario, TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the emulation pinned ON for the whole process, which "
|
||||
"is what the UnlocatedIoBlocks. ctest entry does with "
|
||||
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1; with the variable unset the "
|
||||
"driver POST decides, and on this machine it decides the blocks are "
|
||||
"fine - so there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it stripped anything; the UnlocatedIoBlocks. ctest entry "
|
||||
"sets both";
|
||||
}
|
||||
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||
GTEST_SKIP() << "the strip is DirectGLES's; " << Gl().BackendName()
|
||||
<< " hands the module to the driver as SPIR-V, where Location is how "
|
||||
"interfaces are matched";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote. The latch means it is emitted at the FIRST stage of the
|
||||
// FIRST affected program, which is inside the build below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||
<< Gl().BackendName() << ", so nothing would arm the strip: " << BuildLog();
|
||||
}
|
||||
// Drawn as well as built, so a stack that defers its backend program to first use
|
||||
// still reaches the transpile this is asserting about.
|
||||
const Rgba8 centre = DrawAndReadCentre(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsGreen(centre)) << "the pinned-on lane did not even render correctly: " << centre;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("WITHOUT their layout(location) qualifier"), std::string::npos)
|
||||
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned ON, a five-stage program with four "
|
||||
"interface-block boundaries was built and drawn, and DirectGLES never reported "
|
||||
"stripping a single location. The emulation is not armed - check the override "
|
||||
"mapping in Loader.cpp (it is inverted on purpose) and the arming gate in "
|
||||
"Managers.cpp. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,276 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnwrittenPositionOutputScenario.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A SHADER REDECLARES gl_PerVertex AND NEVER WRITES gl_Position.
|
||||
//
|
||||
// Legal, ordinary GLSL, and until now a process kill on DirectVulkan. The chain, all of it
|
||||
// inside MobileGL's own SPIR-V plumbing:
|
||||
//
|
||||
// 1. glslang emits every DECLARED interface variable, used or not, and lists it on
|
||||
// OpEntryPoint. So `out gl_PerVertex { vec4 gl_Position; };` with no write still produces
|
||||
// the OpVariable, the OpMemberDecorate BuiltIn Position, and an interface slot.
|
||||
// 2. At link, ShaderCompiler::SanitizeAndOptimizeBinary runs AggressiveDCE(remove_outputs =
|
||||
// false) - which may never delete an Output - and then RemoveUnusedInterfaceVariables,
|
||||
// which rebuilds the interface list from the variables instructions actually reference.
|
||||
// The OpVariable and its BuiltIn decoration SURVIVE; the interface slot is DELISTED.
|
||||
// 3. At pipeline build, ProgramFactory picks the last pre-rasterisation stage and runs two
|
||||
// passes over it. GlToVulkanPositionFixPass finds the position target through the
|
||||
// surviving ANNOTATION and injects a load-modify-STORE through it. When gl_Position is in
|
||||
// the transform-feedback capture list, XfbCaptureDecoratePass::MirrorPositionForCapture
|
||||
// also injects an access chain and a LOAD through it.
|
||||
// 4. Either injection is a static use of a variable that is no longer on the entry point's
|
||||
// interface, which is invalid SPIR-V ("Interface variable id <N> is used by entry point
|
||||
// 'main' id <M>, but is not listed as an interface"). Mali r54 does not reject such a
|
||||
// module - it faults inside pipeline creation and takes the process down.
|
||||
//
|
||||
// Measured on a Mali-G1-Ultra as 216 KHR-GL44/45/46.tessellation_shader.tessellation_control_
|
||||
// to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize_* crashes; the CTS's TES
|
||||
// there is exactly the shape below. It is not tessellation-specific and not XFB-specific: a
|
||||
// vertex shader is enough, which is what these cases use.
|
||||
//
|
||||
// Every test captures a USER varying through transform feedback under GL_RASTERIZER_DISCARD.
|
||||
// Position is undefined in the first two by construction, so it is never asserted on - what is
|
||||
// asserted is that the capture came back at all, which it can only do if the driver accepted
|
||||
// the module and built a pipeline.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kCaptureFloats = 4;
|
||||
constexpr GLsizeiptr kCaptureBytes = static_cast<GLsizeiptr>(kCaptureFloats * sizeof(float));
|
||||
|
||||
// The defect's shape: gl_PerVertex redeclared, gl_Position never assigned.
|
||||
constexpr const char* kUnwrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The control that isolates the redeclaration: identical but for the one assignment.
|
||||
// This one keeps its interface slot through the sanitize chain, so both injections were
|
||||
// always legal on it - it must stay working.
|
||||
constexpr const char* kWrittenPositionVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
};
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
// The second control, and the one the CTS calls data_pass_through: no gl_PerVertex
|
||||
// redeclaration at all, so there is no Position annotation for the passes to find and
|
||||
// nothing to delist. It was never affected and proves the crash needs the redeclaration.
|
||||
constexpr const char* kNoPositionBlockVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec4 vs_in_value;
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
vs_out_value = vs_in_value;
|
||||
}
|
||||
)";
|
||||
|
||||
GLuint CompileVertexShader(const std::string& source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// `captureNames` is what goes to glTransformFeedbackVaryings. Passing gl_Position in it
|
||||
// is what puts MirrorPositionForCapture on the path.
|
||||
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::string* log) {
|
||||
const GLuint vertexShader = CompileVertexShader(vertexSource, log);
|
||||
if (vertexShader == 0) return 0;
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(captureNames.size()), captureNames.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
class UnwrittenPositionOutputScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const float vertex[kCaptureFloats] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, vertex, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// Links `vertexSource` with `captureNames`, runs one captured point, and checks that
|
||||
// the USER varying came back. `captureStride` is how many floats one captured vertex
|
||||
// occupies, so the user varying can be read out from behind a captured gl_Position.
|
||||
void ExpectUserVaryingIsCaptured(const char* vertexSource, const std::vector<const char*>& captureNames,
|
||||
std::size_t captureStride, std::size_t userVaryingOffset,
|
||||
const char* what) {
|
||||
std::string log;
|
||||
const GLuint program = BuildCaptureProgram(vertexSource, captureNames, &log);
|
||||
ASSERT_NE(program, 0u) << what << ": the capture program failed to build: " << log;
|
||||
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(captureStride * sizeof(float));
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
// Pre-fill with a value the shader cannot produce, so "captured nothing" is
|
||||
// distinguishable from "captured the wrong thing".
|
||||
const std::vector<float> poison(captureStride, -1.0f);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, captureBytes, poison.data(), GL_DYNAMIC_READ);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << what << ": setting up the capture buffer raised a GL error";
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
glEndTransformFeedback();
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the captured draw raised a GL error";
|
||||
|
||||
std::vector<float> readback(captureStride, -2.0f);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, readback.data());
|
||||
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
|
||||
EXPECT_FLOAT_EQ(readback[userVaryingOffset + i], static_cast<float>(i + 1))
|
||||
<< what << ": captured float " << i << " came back as "
|
||||
<< readback[userVaryingOffset + i]
|
||||
<< "; the pre-fill value means the draw never produced a vertex, which is what an "
|
||||
"invalid shader module looks like from out here";
|
||||
}
|
||||
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The clip fixup's half: PositionZRemap is on for every draw, so the fixup runs on this
|
||||
// program and used to inject a store through the delisted block.
|
||||
TEST_F(UnwrittenPositionOutputScenario, ARedeclaredButUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared, never written");
|
||||
}
|
||||
|
||||
// The XFB half: capturing gl_Position adds an access chain and a LOAD through the same
|
||||
// delisted block, which the interface rule covers exactly as it covers the store. Position
|
||||
// itself is undefined here - only the user varying behind it is asserted.
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAnUnwrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// DirectVulkan only, and not because the defect was backend-specific in principle - the
|
||||
// injection this pins lives in DirectVulkan's ProgramFactory, and DirectGLES cannot
|
||||
// reach the case at all: capturing gl_Position BY NAME off a shader that never writes it
|
||||
// comes back empty there, because the ESSL the transpiler emits has no such output for
|
||||
// the capture list to name. That is a known, separate DirectGLES gap (the same one that
|
||||
// blocks gl_Position/gl_PointSize capture in the tessellation capture segment), tracked
|
||||
// outside this scenario; asserting it here would only re-report it.
|
||||
if (Gl().BackendName() != "DirectVulkan") {
|
||||
GTEST_SKIP() << "capturing an unwritten gl_Position by name is a separate, known "
|
||||
<< "DirectGLES gap; this case pins the DirectVulkan injection";
|
||||
}
|
||||
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing an unwritten gl_Position");
|
||||
}
|
||||
|
||||
// Control: the same shader with the one assignment restored. Its block is never delisted,
|
||||
// so it exercises the path the fixup is actually for and must keep working.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AWrittenRedeclaredPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"redeclared and written");
|
||||
}
|
||||
|
||||
TEST_F(UnwrittenPositionOutputScenario, CapturingAWrittenPositionStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
|
||||
kCaptureFloats * 2, kCaptureFloats, "capturing a written gl_Position");
|
||||
}
|
||||
|
||||
// Control: no gl_PerVertex redeclaration, so no Position annotation and nothing to delist.
|
||||
TEST_F(UnwrittenPositionOutputScenario, AShaderWithNoPositionBlockStillDraws) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ExpectUserVaryingIsCaptured(kNoPositionBlockVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
|
||||
"no gl_PerVertex block");
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -525,7 +525,7 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
//
|
||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
||||
// the SAME program in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// the SAME program in a process started with MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0
|
||||
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
|
||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
||||
@@ -551,10 +551,10 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
// entry for it, so the control still runs in every ctest run; anywhere else - the
|
||||
// ambient ctest entries, or the binary run straight from a device shell - the
|
||||
// emulation is on and this case skips.
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the "
|
||||
"whole process; the NoViewportArrayEmulation. ctest entry runs it with "
|
||||
"MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
"MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0";
|
||||
}
|
||||
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A CAPTURE MUST STILL RECORD WHEN IT IS NOT THE FIRST ONE IN THE PROCESS,
|
||||
// AND THE CAPTURE STAGE MAY BE ANY OF THE FOUR THAT CAN BE THE LAST ONE.
|
||||
//
|
||||
// The conformance suite exposed a whole family of transform feedback failures that no
|
||||
// existing scenario could reproduce, because every one of them ran ONE capture, from a
|
||||
// VERTEX stage, in a freshly initialised process. What the suite actually does is
|
||||
// different in three ways at once, and each of them turned out to matter:
|
||||
//
|
||||
// * it runs case after case in ONE GL context, resetting state between them - and the
|
||||
// reset is not a fresh context. Its transform feedback part
|
||||
// (framework/opengl/gluStateReset.cpp resetStateGLCore) unbinds the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER and then clears every indexed capture point from 0 to
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, which permanently raises MobileGL's
|
||||
// touched-binding-point high-water mark. Every later capture that uses fewer points
|
||||
// than that - i.e. every INTERLEAVED_ATTRIBS capture - then had the unused tail
|
||||
// re-cleared on the driver immediately before glBeginTransformFeedback.
|
||||
// ReplayDeqpStateReset below is that reset, reduced to the calls that touch capture
|
||||
// state, so a defect that only appears from the second capture onwards is reachable
|
||||
// here instead of only on a device.
|
||||
//
|
||||
// * the capture stage is frequently a GEOMETRY or a TESSELLATION EVALUATION shader,
|
||||
// never a plain vertex shader. The tree had zero coverage for either: none of the
|
||||
// Xfb* scenarios mentioned tessellation and neither TessellationDrawModeScenario nor
|
||||
// GeometryDrawModeScenario mentioned transform feedback.
|
||||
//
|
||||
// * the capture program frequently has NO FRAGMENT STAGE at all, because it draws
|
||||
// under GL_RASTERIZER_DISCARD and never rasterises anything. That is legal in
|
||||
// desktop GL and the shape most "use transform feedback as a readback channel"
|
||||
// tests are built on.
|
||||
//
|
||||
// Every case here asserts the captured BYTES, never just the absence of a GL error: the
|
||||
// failure this guards against writes nothing and raises nothing, so a buffer that kept
|
||||
// its poison is the only thing that distinguishes it from success.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Nothing a capture can legitimately produce, so a component that still reads it
|
||||
// names the failure ("the capture never reached these bytes") instead of looking
|
||||
// like an ordinary numeric mismatch.
|
||||
constexpr int kPoison = -987654;
|
||||
|
||||
const char* const kPassthroughVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in int vs_in_value;
|
||||
flat out int vs_out_value;
|
||||
void main()
|
||||
{
|
||||
vs_out_value = vs_in_value;
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The primitive_counter shape: one flat int per emitted vertex, several vertices
|
||||
// per input primitive, so the capture is geometry-AMPLIFIED and the CPU-side
|
||||
// primitive model cannot predict its length.
|
||||
const char* const kPointAmplifyingGeometrySource = R"(#version 420 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 2) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
gs_out_value = vs_out_value[0];
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Adjacency input. Only a geometry stage can consume it, and CountPrimitivesForDraw
|
||||
// used to answer 0 for every adjacency mode, which silently excluded the whole draw
|
||||
// from the capture accounting.
|
||||
const char* const kAdjacencyGeometrySource = R"(#version 420 core
|
||||
layout(lines_adjacency) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
flat out int gs_out_value;
|
||||
void main()
|
||||
{
|
||||
gs_out_value = vs_out_value[1];
|
||||
gl_Position = gl_in[1].gl_Position;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessControlSource = R"(#version 420 core
|
||||
layout(vertices = 1) out;
|
||||
flat in int vs_out_value[];
|
||||
patch out int tcs_out_value;
|
||||
void main()
|
||||
{
|
||||
tcs_out_value = vs_out_value[0];
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
patch in int tcs_out_value;
|
||||
flat out int tes_out_value;
|
||||
void main()
|
||||
{
|
||||
tes_out_value = tcs_out_value;
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
flat in int gs_out_value;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(float(gs_out_value), 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class XfbRepeatedCaptureScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
const int values[kInputVertices] = {10, 11, 12, 13};
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(values), values, GL_STATIC_DRAW);
|
||||
glVertexAttribIPointer(0, 1, GL_INT, 0, nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vbo = 0;
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static constexpr int kInputVertices = 4;
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 2;
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
// The transform-feedback-relevant half of deqp's resetStateGLCore, in its order.
|
||||
// It runs between EVERY pair of conformance cases, and running one capture
|
||||
// through it is the difference between "the first capture in the process" and
|
||||
// every other one.
|
||||
static void ReplayDeqpStateReset() {
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
GLint maxSeparateAttribs = 0;
|
||||
glGetIntegerv(GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSeparateAttribs);
|
||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||
for (GLint index = 0; index < maxSeparateAttribs; ++index) {
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(index), 0);
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const char* varying) {
|
||||
return BuildCaptureProgram(stages, std::vector<const char*>{varying});
|
||||
}
|
||||
|
||||
// Builds a capture program out of `stages` capturing `varyings` interleaved.
|
||||
// Returns 0 and fills m_buildLog on failure.
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
||||
GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span. `captureMode` is the transform feedback primitive mode,
|
||||
// `drawMode`/`count` the draw. Returns the capture buffer's contents.
|
||||
std::vector<int> RunCaptureSpan(GLuint program, GLenum captureMode, GLenum drawMode, GLsizei count,
|
||||
std::size_t capturedInts) {
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// The capture point is the ONLY thing bound; the generic
|
||||
// GL_TRANSFORM_FEEDBACK_BUFFER binding comes along for the ride, exactly as
|
||||
// the conformance tests rely on (GL 4.6 core 6.1.1).
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(captureMode);
|
||||
glDrawArrays(drawMode, 0, count);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<int> readback(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
glUseProgram(0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedNothing(const std::vector<int>& data) {
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != kPoison) {
|
||||
return ::testing::AssertionFailure() << "component " << i << " is " << data[i];
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult CapturedIs(const std::vector<int>& data,
|
||||
const std::vector<int>& expected) {
|
||||
if (data.size() != expected.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "captured " << data.size() << " value(s), expected " << expected.size();
|
||||
}
|
||||
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != expected[i]) {
|
||||
::testing::AssertionResult failure = ::testing::AssertionFailure();
|
||||
failure << "component " << i << " is " << data[i] << ", expected " << expected[i];
|
||||
if (data[i] == kPoison) {
|
||||
failure << " (the capture never reached these bytes)";
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// THE REGRESSION GUARD FOR THE WHOLE FAMILY. Two geometry-stage captures in one
|
||||
// process with the conformance suite's own state reset between them; the assertion
|
||||
// that matters is on the SECOND one, which is the one every device run failed while
|
||||
// whichever body happened to land first in its process passed.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASecondGeometryCaptureAfterADeqpStateResetStillRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
// Two vertices emitted per input point, so the capture is amplified beyond what
|
||||
// the CPU primitive model can predict from the draw alone.
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
|
||||
for (int capture = 0; capture < 3; ++capture) {
|
||||
// A fresh program per capture, because that is what a fresh conformance case
|
||||
// builds - and it is what makes the driver recycle program and buffer names.
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture " << capture << " program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected))
|
||||
<< "capture " << capture << " of 3 in this process"
|
||||
<< (capture == 0 ? "" : " (every earlier one was followed by a deqp-shaped state reset)");
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "capture " << capture;
|
||||
|
||||
glDeleteProgram(program);
|
||||
m_programs.pop_back();
|
||||
ReplayDeqpStateReset();
|
||||
glBindVertexArray(m_vao);
|
||||
}
|
||||
}
|
||||
|
||||
// The tessellation half, which had no coverage anywhere in the tree: a capture taken
|
||||
// from a GL_PATCHES draw, whose last vertex-processing stage is the evaluation shader
|
||||
// and whose record count only the tessellator knows.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAPatchesDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< ")";
|
||||
}
|
||||
|
||||
// One input patch of one vertex, all levels at 1: the tessellator emits exactly
|
||||
// one triangle, so three captured vertices all carrying the first input value.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
|
||||
"tes_out_value");
|
||||
ASSERT_NE(program, 0u) << "patch capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 10};
|
||||
const std::vector<int> captured = RunCaptureSpan(program, GL_TRIANGLES, GL_PATCHES, 1, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A capture program with NO FRAGMENT STAGE, drawn under GL_RASTERIZER_DISCARD. Legal
|
||||
// in desktop GL, and the shape most transform-feedback-as-readback tests use; the
|
||||
// program above only differs from it by the fragment shader, so a failure here is
|
||||
// specifically about the missing stage.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAFragmentlessProgramRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "fragmentless capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An ADJACENCY draw feeding the capture. CountPrimitivesForDraw answered 0 for all
|
||||
// four adjacency modes, which made the transform feedback accounting skip the draw
|
||||
// entirely - so neither the captured-vertex counter nor the geometry-capture-draw
|
||||
// flag moved, and anything downstream of either was working from "nothing happened".
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAnAdjacencyDrawRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kAdjacencyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "adjacency capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line primitive; the shader emits
|
||||
// the second vertex of the four, which is the line's first real endpoint.
|
||||
const std::vector<int> expected = {11};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_POINTS, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// An adjacency draw with NO geometry stage. GL 4.6 core table 13.1 admits
|
||||
// GL_LINES_ADJACENCY and GL_LINE_STRIP_ADJACENCY under capture mode GL_LINES (and the
|
||||
// triangle pair under GL_TRIANGLES): without a geometry shader the adjacent vertices
|
||||
// are ignored and the primitive assembled is a plain line, so the combination is legal
|
||||
// and must capture. MobileGL's active-capture primitive-mode table listed only the
|
||||
// non-adjacency modes, so this raised GL_INVALID_OPERATION and dropped the draw
|
||||
// entirely - the buffer kept its pre-draw bytes and the application saw an error the
|
||||
// spec does not allow. Distinct from ACaptureFromAnAdjacencyDrawRecords above, which
|
||||
// HAS a geometry stage and therefore bypasses that table completely.
|
||||
TEST_F(XfbRepeatedCaptureScenario, AVertexOnlyAdjacencyCaptureRecords) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const GLuint program =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(program, 0u) << "vertex-only capture program failed to build: " << m_buildLog;
|
||||
|
||||
// Four vertices of GL_LINES_ADJACENCY are one line whose real endpoints are the
|
||||
// middle pair, so the capture is those two vertices in order.
|
||||
const std::vector<int> expected = {11, 12};
|
||||
const std::vector<int> captured =
|
||||
RunCaptureSpan(program, GL_LINES, GL_LINES_ADJACENCY, kInputVertices, expected.size());
|
||||
|
||||
// THE GUARD FOR THE DEFECT ITSELF, and it is backend-independent: the frontend
|
||||
// validator must not reject the combination. It used to record
|
||||
// GL_INVALID_OPERATION and return before the draw was ever issued.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR)
|
||||
<< "a capture-mode/draw-mode pair GL 4.6 core table 13.1 admits must raise no error";
|
||||
|
||||
// Whether the capture then RECORDS is a backend question, and the two answer it
|
||||
// differently. ES 3.2 (10.1) supports the adjacency primitive types only for a
|
||||
// pipeline with a geometry shader, so DirectGLES has nothing to forward this draw
|
||||
// to; desktop GL and Vulkan both assemble the plain line and capture it. Asserting
|
||||
// the data unconditionally would be asserting that DirectGLES emulates a whole ES
|
||||
// restriction away, which is a separate piece of work and not what this guards.
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "DirectGLES cannot forward a geometry-shader-less adjacency draw: ES 3.2 10.1 "
|
||||
"supports the adjacency primitive types only with a geometry stage. The frontend "
|
||||
"no longer rejects the draw (checked above), which is the defect this covers.";
|
||||
}
|
||||
EXPECT_TRUE(CapturedIs(captured, expected));
|
||||
}
|
||||
|
||||
// A CAPTURE MUST NEVER LAND IN A BUFFER THE APPLICATION DID NOT BIND FOR IT.
|
||||
//
|
||||
// A capture list may legally begin with gl_NextBuffer, which leaves capture buffer 0
|
||||
// with stride 0 and nothing to capture - so glBeginTransformFeedback does not require a
|
||||
// buffer at point 0 and the application binds only point 1. The driver-side program is
|
||||
// a single-buffer interleaved capture (the pseudo-varyings are consumed at link time),
|
||||
// so it writes capture point 0, and MobileGL redirects that into scratch storage and
|
||||
// scatters the records afterwards.
|
||||
//
|
||||
// Two ways that went wrong, both fixed here: the scratch was sized by reading each
|
||||
// target's stride at its POSITION in a list that skips unbound buffers, which for this
|
||||
// layout read stride 0 for everything and produced a zero capacity; and when the
|
||||
// scratch then failed to bind, the span opened anyway onto whatever capture point 0
|
||||
// still held from an earlier capture in the process - silently overwriting an unrelated
|
||||
// application buffer. The first span below exists purely to leave such a binding behind.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ACaptureListBeginningWithGlNextBufferSparesTheEarlierBuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::size_t capturedInts = 4;
|
||||
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(capturedInts * sizeof(int));
|
||||
|
||||
// Span A: an ordinary capture, so capture point 0 is left holding bufferA.
|
||||
const GLuint programA =
|
||||
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
|
||||
ASSERT_NE(programA, 0u) << "plain capture program failed to build: " << m_buildLog;
|
||||
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint bufferA = 0;
|
||||
glGenBuffers(1, &bufferA);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, bufferA);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(programA);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> afterA(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, afterA.data());
|
||||
const std::vector<int> spanAExpected = {10, 11, 12, 13};
|
||||
ASSERT_TRUE(CapturedIs(afterA, spanAExpected)) << "the setup span itself did not capture";
|
||||
|
||||
// Span B: gl_NextBuffer first, so buffer 0 captures nothing and only point 1 is bound.
|
||||
const GLuint programB = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}},
|
||||
{"gl_NextBuffer", "vs_out_value"});
|
||||
if (programB == 0) {
|
||||
GTEST_SKIP() << "gl_NextBuffer capture lists are not linkable on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << "): " << m_buildLog;
|
||||
}
|
||||
|
||||
GLuint bufferB = 0;
|
||||
glGenBuffers(1, &bufferB);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferB);
|
||||
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
// Point 0 released, point 1 is the only destination this capture asks for.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, bufferB);
|
||||
|
||||
glUseProgram(programB);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(GL_POINTS, 0, kInputVertices);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
glUseProgram(0);
|
||||
|
||||
// THE ASSERTION THAT MATTERS: bufferA was not a destination of this capture, so it
|
||||
// must still read exactly what span A left in it. A failure here is the corruption.
|
||||
std::vector<int> bufferAAfterB(capturedInts, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, captureBytes, bufferAAfterB.data());
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
EXPECT_TRUE(CapturedIs(bufferAAfterB, spanAExpected))
|
||||
<< "the gl_NextBuffer capture wrote into the buffer the PREVIOUS span had bound";
|
||||
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and, where the backend places this layout at all, the buffer it WAS asked to
|
||||
// write gets the records. That placement is the DirectGLES scatter path, whose
|
||||
// scratch sizing used to read each target's stride at its POSITION in a list that
|
||||
// skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0
|
||||
// for every target and sized the scratch at zero. DirectVulkan does not implement a
|
||||
// leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a
|
||||
// pre-existing gap of its own, and the assertion above - that it corrupts nothing
|
||||
// while declining - is what matters for it.
|
||||
const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan";
|
||||
if (backendPlacesLeadingNextBuffer) {
|
||||
std::vector<int> bufferBAfter(capturedInts, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data());
|
||||
EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected));
|
||||
}
|
||||
|
||||
// Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer
|
||||
// this test deleted would follow the process into the next scenario.
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0);
|
||||
glDeleteBuffers(1, &bufferA);
|
||||
glDeleteBuffers(1, &bufferB);
|
||||
|
||||
if (!backendPlacesLeadingNextBuffer) {
|
||||
GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it "
|
||||
"captures nothing, which the no-corruption assertion above has already covered.";
|
||||
}
|
||||
}
|
||||
|
||||
// The control for all of the above: a span that never draws must leave the capture
|
||||
// buffer alone. Without it "the buffer kept its poison" could be read as the correct
|
||||
// outcome of some path rather than as the bug, and the tightened early returns in
|
||||
// StartPendingTransformFeedback have to keep this legal case legal.
|
||||
TEST_F(XfbRepeatedCaptureScenario, ASpanThatNeverDrawsLeavesTheCaptureBufferAlone) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
"gs_out_value");
|
||||
ASSERT_NE(program, 0u) << "capture program failed to build: " << m_buildLog;
|
||||
|
||||
const std::size_t capturedInts = 8;
|
||||
std::vector<int> poison(capturedInts, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
|
||||
GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glUseProgram(program);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glEndTransformFeedback();
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<int> readback(capturedInts, 0);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
|
||||
EXPECT_TRUE(CapturedNothing(readback));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -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
|
||||
@@ -0,0 +1,549 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeValueTypes.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
|
||||
#ifndef MOBILEGL_MG_PIPE_VALUE_TYPES_H // belt and braces: this file is reachable both as
|
||||
#define MOBILEGL_MG_PIPE_VALUE_TYPES_H // <MG_Pipe/...> and <...> (CMakeLists.txt:531,535)
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Math/VectorTypes.h> // includes only <Includes.h> + <cstring>
|
||||
#include <cstddef> // offsetof
|
||||
#include <type_traits>
|
||||
|
||||
// The value types MG_Pipe payloads embed (plan B section 6.3; ARCHITECTURE.md section on
|
||||
// the value header): the render-state, pixel-store, sampler and vertex-attribute value
|
||||
// structs and the enums they are made of. They lived in MG_State::GLState until P0.5;
|
||||
// the MG_State headers that used to define them now include this file, so every existing
|
||||
// spelling (namespace and name) compiles unchanged.
|
||||
//
|
||||
// PURITY: nothing from MG_State, MG_Impl, MG_Backend or MG_Remote -
|
||||
// scripts/check_include_closure.py probe "value-header" (ROADMAP P0.5; ARCHITECTURE.md
|
||||
// section 10.3 gate A). Adding one turns CI red. MG_Pipe never includes MG_State back.
|
||||
|
||||
namespace MobileGL {
|
||||
// GL_MAX_DRAW_BUFFERS as MobileGL advertises it. FramebufferObject::MAX_DRAW_BUFFERS is
|
||||
// defined from this constant, so the two cannot drift.
|
||||
inline constexpr Uint kMGMaxDrawBuffers = 8;
|
||||
|
||||
enum class BlendFactor {
|
||||
Zero,
|
||||
One,
|
||||
SrcColor,
|
||||
OneMinusSrcColor,
|
||||
DstColor,
|
||||
OneMinusDstColor,
|
||||
SrcAlpha,
|
||||
OneMinusSrcAlpha,
|
||||
DstAlpha,
|
||||
OneMinusDstAlpha,
|
||||
ConstantColor,
|
||||
OneMinusConstantColor,
|
||||
ConstantAlpha,
|
||||
OneMinusConstantAlpha,
|
||||
// Dual-source blend factors (GL_SRC1_*, glBindFragDataLocationIndexed); require the
|
||||
// dualSrcBlend device feature.
|
||||
Src1Color,
|
||||
OneMinusSrc1Color,
|
||||
Src1Alpha,
|
||||
OneMinusSrc1Alpha,
|
||||
BlendFactorCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class BlendEquation {
|
||||
Add,
|
||||
Subtract,
|
||||
ReverseSubtract,
|
||||
Min,
|
||||
Max,
|
||||
BlendEquationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class LogicOperation {
|
||||
Clear,
|
||||
And,
|
||||
AndReverse,
|
||||
Copy,
|
||||
AndInverted,
|
||||
Noop,
|
||||
Xor,
|
||||
Or,
|
||||
Nor,
|
||||
Equiv,
|
||||
Invert,
|
||||
OrReverse,
|
||||
CopyInverted,
|
||||
OrInverted,
|
||||
Nand,
|
||||
Set,
|
||||
LogicOperationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class DepthTestFunc {
|
||||
Never,
|
||||
Less,
|
||||
Equal,
|
||||
LessEqual,
|
||||
Greater,
|
||||
NotEqual,
|
||||
GreaterEqual,
|
||||
Always,
|
||||
DepthTestFuncCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class StencilOperation {
|
||||
Keep,
|
||||
Zero,
|
||||
Replace,
|
||||
IncrementClamp,
|
||||
DecrementClamp,
|
||||
Invert,
|
||||
IncrementWrap,
|
||||
DecrementWrap,
|
||||
StencilOperationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class StencilFace {
|
||||
Front,
|
||||
Back,
|
||||
StencilFaceCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class PixelStoreParam {
|
||||
// Pack Parameters
|
||||
PackAlignment,
|
||||
PackRowLength,
|
||||
PackImageHeight,
|
||||
PackSkipRows,
|
||||
PackSkipPixels,
|
||||
PackSkipImages,
|
||||
PackSwapBytes,
|
||||
PackLSBFirst,
|
||||
|
||||
// Unpack Parameters
|
||||
UnpackAlignment,
|
||||
UnpackRowLength,
|
||||
UnpackImageHeight,
|
||||
UnpackSkipRows,
|
||||
UnpackSkipPixels,
|
||||
UnpackSkipImages,
|
||||
UnpackSwapBytes,
|
||||
UnpackLSBFirst,
|
||||
|
||||
PixelStoreParamCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class CullFaceMode {
|
||||
Front,
|
||||
Back,
|
||||
FrontAndBack,
|
||||
CullFaceModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class FrontFaceMode {
|
||||
CounterClockwise,
|
||||
Clockwise,
|
||||
FrontFaceModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class ProvokingVertexMode {
|
||||
FirstVertex,
|
||||
LastVertex,
|
||||
ProvokingVertexModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class CapabilityInput {
|
||||
Blend,
|
||||
ClipDistance0,
|
||||
ClipDistance1,
|
||||
ClipDistance2,
|
||||
ClipDistance3,
|
||||
ClipDistance4,
|
||||
ClipDistance5,
|
||||
ClipDistance6,
|
||||
ClipDistance7,
|
||||
ColorLogicOp,
|
||||
CullFace,
|
||||
DebugOutput,
|
||||
DebugOutputSynchronous,
|
||||
DepthClamp,
|
||||
DepthTest,
|
||||
Dither,
|
||||
FramebufferSrgb,
|
||||
LineSmooth,
|
||||
Multisample,
|
||||
PolygonOffsetFill,
|
||||
PolygonOffsetLine,
|
||||
PolygonOffsetPoint,
|
||||
PolygonSmooth,
|
||||
PrimitiveRestart,
|
||||
PrimitiveRestartFixedIndex,
|
||||
RasterizerDiscard,
|
||||
SampleAlphaToCoverage,
|
||||
SampleAlphaToOne,
|
||||
SampleCoverage,
|
||||
SampleShading,
|
||||
SampleMask,
|
||||
ScissorTest,
|
||||
StencilTest,
|
||||
TextureCubeMapSeamless,
|
||||
ProgramPointSize,
|
||||
CapabilityInputCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
struct PixelStoreParameters {
|
||||
Bool SwapBytes = false;
|
||||
Bool LSBFirst = false;
|
||||
Int RowLength = 0;
|
||||
Int ImageHeight = 0;
|
||||
Int SkipPixels = 0;
|
||||
Int SkipRows = 0;
|
||||
Int SkipImages = 0;
|
||||
Int Alignment = 4;
|
||||
};
|
||||
|
||||
struct PerBufferBlendState {
|
||||
Bool Enabled = false;
|
||||
BlendFactor SrcFactorRGB = BlendFactor::One;
|
||||
BlendFactor DstFactorRGB = BlendFactor::Zero;
|
||||
BlendFactor SrcFactorAlpha = BlendFactor::One;
|
||||
BlendFactor DstFactorAlpha = BlendFactor::Zero;
|
||||
BlendEquation ColorEquation = BlendEquation::Add;
|
||||
BlendEquation AlphaEquation = BlendEquation::Add;
|
||||
};
|
||||
|
||||
struct StencilFaceState {
|
||||
DepthTestFunc Func = DepthTestFunc::Always;
|
||||
Int Ref = 0;
|
||||
Uint32 ValueMask = 0xffffffffu;
|
||||
Uint32 WriteMask = 0xffffffffu;
|
||||
StencilOperation FailOp = StencilOperation::Keep;
|
||||
StencilOperation PassDepthFailOp = StencilOperation::Keep;
|
||||
StencilOperation PassDepthPassOp = StencilOperation::Keep;
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
Uint PatchVertices = 3;
|
||||
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL (glPatchParameterfv). The
|
||||
// tessellation levels used when a program has an evaluation stage and NO control stage -
|
||||
// GL's fixed-function pass-through (4.6 core 11.2.2). Both backends have to synthesize
|
||||
// that stage, and they bake these numbers into it, so a change here makes an already-built
|
||||
// one stale exactly as PATCH_VERTICES does. Default 1.0, per table 23.44.
|
||||
FloatVec4 PatchDefaultOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 PatchDefaultInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Float PolygonOffsetFactor = 0.0f;
|
||||
Float PolygonOffsetUnits = 0.0f;
|
||||
// GL_POLYGON_OFFSET_CLAMP (GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp): the maximum
|
||||
// magnitude of the offset glPolygonOffsetClamp's third argument allows. Zero - the default
|
||||
// - means "no clamp", which is exactly the behaviour glPolygonOffset leaves behind.
|
||||
Float PolygonOffsetClamp = 0.0f;
|
||||
|
||||
// glClipControl (GL 4.5 core 13.5). Defaults per table 23.7 are the pre-4.5 fixed
|
||||
// behaviour: origin at the lower left, depth mapped from -1..1.
|
||||
GLenum ClipOrigin = GL_LOWER_LEFT;
|
||||
GLenum ClipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
|
||||
|
||||
// Blending
|
||||
Array<PerBufferBlendState, kMGMaxDrawBuffers> BlendStates;
|
||||
LogicOperation LogicOp = LogicOperation::Copy;
|
||||
|
||||
// Depth
|
||||
Bool DepthTestEnabled = false;
|
||||
DepthTestFunc DepthFunc = DepthTestFunc::Less;
|
||||
Bool DepthMask = true;
|
||||
|
||||
// Color Mask. Per-draw-buffer state (glColorMaski); glColorMask broadcasts to all buffers.
|
||||
// Every entry is initialized to all-true in RenderState's constructor.
|
||||
Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
|
||||
|
||||
// Clear State
|
||||
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
// glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples
|
||||
// that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial
|
||||
// value is 0, and the value is clamped to [0, 1] on the way in.
|
||||
Float MinSampleShadingValue = 0.0f;
|
||||
Array<StencilFaceState, 2> StencilStates{};
|
||||
|
||||
// Cull Face
|
||||
Bool CullFaceEnabled = false;
|
||||
CullFaceMode CullFaceModeSetting = CullFaceMode::Back;
|
||||
FrontFaceMode FrontFaceModeSetting = FrontFaceMode::CounterClockwise;
|
||||
ProvokingVertexMode ProvokingVertexModeSetting = ProvokingVertexMode::LastVertex;
|
||||
|
||||
// Hints (glHint). All GL 3.3 core hint targets default to GL_DONT_CARE.
|
||||
GLenum LineSmoothHint = GL_DONT_CARE;
|
||||
GLenum PolygonSmoothHint = GL_DONT_CARE;
|
||||
GLenum TextureCompressionHint = GL_DONT_CARE;
|
||||
GLenum FragmentShaderDerivativeHint = GL_DONT_CARE;
|
||||
|
||||
// Point parameters (glPointParameter). Only the two GL 3.3 core pnames.
|
||||
Float PointFadeThresholdSize = 1.0f;
|
||||
GLenum PointSpriteCoordOrigin = GL_UPPER_LEFT;
|
||||
|
||||
// Color clamping (glClampColor). Core profile exposes only GL_CLAMP_READ_COLOR.
|
||||
GLenum ClampReadColor = GL_FIXED_ONLY;
|
||||
|
||||
// Polygon rasterization mode (glPolygonMode). Core profile sets front and back together,
|
||||
// but GL_POLYGON_MODE still reports both slots, so keep them separate for a faithful query.
|
||||
GLenum PolygonModeFront = GL_FILL;
|
||||
GLenum PolygonModeBack = GL_FILL;
|
||||
|
||||
// Primitive restart index (glPrimitiveRestartIndex); consumed when GL_PRIMITIVE_RESTART is
|
||||
// enabled during an indexed draw. Default 0.
|
||||
Uint32 PrimitiveRestartIndex = 0;
|
||||
|
||||
// Scissor
|
||||
Bool ColorLogicOpEnabled = false;
|
||||
Bool DebugOutputEnabled = false;
|
||||
Bool DebugOutputSynchronousEnabled = false;
|
||||
Bool DitherEnabled = true;
|
||||
Bool LineSmoothEnabled = false;
|
||||
Bool MultisampleEnabled = true;
|
||||
Bool PolygonOffsetFillEnabled = false;
|
||||
Bool PolygonOffsetLineEnabled = false;
|
||||
Bool PolygonOffsetPointEnabled = false;
|
||||
Bool PolygonSmoothEnabled = false;
|
||||
Bool PrimitiveRestartEnabled = false;
|
||||
Bool PrimitiveRestartFixedIndexEnabled = false;
|
||||
Bool RasterizerDiscardEnabled = false;
|
||||
Bool SampleAlphaToCoverageEnabled = false;
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool SampleShadingEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// One bit per viewport, set the first time the application writes that index's scissor
|
||||
// rectangle - glScissor broadcasts and sets all 16, glScissorIndexed/glScissorArrayv set
|
||||
// the indices they name. It exists because the RECTANGLE cannot answer "has the
|
||||
// application spoken?": ScissorBoxes starts all-zero (its spec initial value is the size
|
||||
// of a window the frontend does not know yet, see the RenderState constructor), and
|
||||
// glScissor(0, 0, 0, 0) is a legal GL state meaning "the scissor test rejects every
|
||||
// fragment". A backend that reads an empty rectangle as the never-written sentinel
|
||||
// therefore INVERTS that request into "accept every fragment"; DirectGLES did exactly
|
||||
// that and KHR-GL43.viewport_array.scissor_zero_dimension caught it. Deliberately beside
|
||||
// ScissorBoxes so it shares their tail span (after LogicOp) and DirectGLES' span memcmp
|
||||
// picks a transition up like any other state.
|
||||
Uint32 ScissorBoxWrittenMask = 0;
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
// eight numbered capabilities cannot share. Lives in the tail span (after LogicOp), so
|
||||
// DirectGLES' span memcmp picks a change up like any other capability.
|
||||
Uint32 ClipDistanceEnabledMask = 0;
|
||||
};
|
||||
|
||||
enum class SamplerFilterMode {
|
||||
Nearest,
|
||||
Linear,
|
||||
SamplerFilterCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerMipmapMode {
|
||||
None,
|
||||
Nearest,
|
||||
Linear,
|
||||
SamplerMipmapModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerWrapMode {
|
||||
ClampToEdge,
|
||||
MirroredRepeat,
|
||||
Repeat,
|
||||
ClampToBorder,
|
||||
MirrorClampToEdge,
|
||||
SamplerWrapModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerCompareMode {
|
||||
None,
|
||||
CompareToTexture,
|
||||
SamplerCompareModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerCompareFunc {
|
||||
Never,
|
||||
Less,
|
||||
Equal,
|
||||
LessEqual,
|
||||
Greater,
|
||||
NotEqual,
|
||||
GreaterEqual,
|
||||
Always,
|
||||
SamplerCompareFuncCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
// Which of the three GL_TEXTURE_BORDER_COLOR entry-point families last wrote the border colour,
|
||||
// and therefore which of the three stored representations is AUTHORITATIVE. GL 4.6 core 8.10:
|
||||
// TexParameterIiv/Iuiv store an integer border colour "unmodified, with an internal data type of
|
||||
// integer", TexParameterfv stores a floating-point one, and the derived forms are only a
|
||||
// convenience for a getter of the other spelling. A backend cannot pick the right driver entry
|
||||
// point (glSamplerParameterIiv vs fv) or the right VkBorderColor family without this: numerically
|
||||
// the three representations are always populated, so the value alone says nothing about the form.
|
||||
enum class BorderColorForm : Uint8 {
|
||||
Float,
|
||||
Int,
|
||||
Uint
|
||||
};
|
||||
|
||||
struct SamplerParameters {
|
||||
SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
|
||||
SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
|
||||
SamplerWrapMode wrapR = SamplerWrapMode::Repeat;
|
||||
SamplerFilterMode minFilter = SamplerFilterMode::Nearest;
|
||||
SamplerFilterMode magFilter = SamplerFilterMode::Linear;
|
||||
SamplerMipmapMode mipmapMode = SamplerMipmapMode::Linear;
|
||||
Float minLod = -1000.0f;
|
||||
Float maxLod = 1000.0f;
|
||||
Float lodBias = 0.0f;
|
||||
Float maxAnisotropy = 1.0f;
|
||||
// GL 4.6 core table 23.18 / GLES 3.2 table 21.16: TEXTURE_COMPARE_FUNC starts at LEQUAL,
|
||||
// for both sampler objects and the sampler state a texture object carries.
|
||||
SamplerCompareFunc compareFunc = SamplerCompareFunc::LessEqual;
|
||||
SamplerCompareMode compareMode = SamplerCompareMode::None;
|
||||
// TEXTURE_BORDER_COLOR is sampler state (GL 4.6 core table 23.18), so it belongs here and
|
||||
// not on the texture - a texture object reaches it through the sampler object it owns. The
|
||||
// three representations are the float, integer and unsigned-integer forms glSamplerParameterfv,
|
||||
// glSamplerParameterIiv and glSamplerParameterIuiv set; whichever is written last defines
|
||||
// the colour and the other two follow it, so a getter always has an answer.
|
||||
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 borderColorI = {0, 0, 0, 0};
|
||||
UintVec4 borderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm borderColorForm = BorderColorForm::Float;
|
||||
};
|
||||
|
||||
namespace MG_State::GLState {
|
||||
class BufferObject;
|
||||
|
||||
struct VertexAttribute {
|
||||
Bool Enabled = false;
|
||||
int Size = 4;
|
||||
DataType Type = DataType::Float32;
|
||||
Bool Normalized = false;
|
||||
// The RESOLVED byte distance between consecutive elements, never the raw
|
||||
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
|
||||
// packed" and is resolved to the element size here, so a zero that survives
|
||||
// into this field can only have come from the binding model, where a zero
|
||||
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
|
||||
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
|
||||
// consume this verbatim; collapsing 0 back into the element size is what made
|
||||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
||||
int Stride = 0;
|
||||
SizeT Offset = 0;
|
||||
Bool IsInteger = false;
|
||||
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
|
||||
// Set only by the long (L) format entry points. It is NOT implied by
|
||||
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
|
||||
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
|
||||
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
|
||||
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
|
||||
Bool IsLong = false;
|
||||
Bool IsBgra = false;
|
||||
Uint Divisor = 0;
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
|
||||
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
|
||||
// arguments of the last glVertexAttrib*Pointer call on this attribute,
|
||||
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
|
||||
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
|
||||
// and the binding model does overwrite those, so the two views have to be
|
||||
// stored apart or the binding-model sequence reports a legacy state it never
|
||||
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
|
||||
int LegacyStride = 0;
|
||||
SizeT LegacyPointer = 0;
|
||||
};
|
||||
|
||||
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
||||
// binding-point API are resolved eagerly into the flat VertexAttribute view above, so
|
||||
// backends keep consuming resolved attributes and never see binding points.
|
||||
struct VertexBufferBindingPoint {
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
SizeT Offset = 0;
|
||||
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
||||
int Stride = 16;
|
||||
Uint Divisor = 0;
|
||||
};
|
||||
|
||||
struct VertexAttributeVersion {
|
||||
Uint16 FormatVersion = 0;
|
||||
Uint16 BufferVersion = 0;
|
||||
Uint16 SwitchVersion = 0;
|
||||
};
|
||||
} // namespace MG_State::GLState
|
||||
|
||||
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
|
||||
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
|
||||
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
|
||||
// therefore not trivially copyable (MGPipeTypes.h carries them as a blob).
|
||||
static_assert(std::is_trivially_copyable_v<PixelStoreParameters> && sizeof(PixelStoreParameters) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<PerBufferBlendState> && sizeof(PerBufferBlendState) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<StencilFaceState> && sizeof(StencilFaceState) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<RenderStateParameters>);
|
||||
static_assert(std::is_standard_layout_v<RenderStateParameters>); // offsetof legality
|
||||
static_assert(sizeof(RenderStateParameters) == 1168,
|
||||
"RenderStateParameters changed size; MGL_RESIDUAL_BLOCK_SIZE and the Espryt spans depend on it");
|
||||
static_assert(offsetof(RenderStateParameters, BlendStates) < offsetof(RenderStateParameters, LogicOp));
|
||||
static_assert(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)> == kMGMaxDrawBuffers);
|
||||
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
|
||||
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
|
||||
sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
|
||||
} // namespace MobileGL
|
||||
#endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H
|
||||
@@ -0,0 +1,177 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeCalls.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 single source of truth for the MGPipe call catalogue (plan B section 4.1 / 4.4 /
|
||||
// appendix A). One line per call; seven generators consume this file
|
||||
// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test
|
||||
// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic.
|
||||
//
|
||||
// X(Name, PayloadStruct, Class, Flags)
|
||||
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
|
||||
// kScreen lands in struct MGPipeScreen, every other class in struct
|
||||
// MGPipeContext (plan section 4.3).
|
||||
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
|
||||
//
|
||||
// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their
|
||||
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
|
||||
// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire
|
||||
// opcode is the 1-based position in this list, so reordering is a protocol break.
|
||||
//
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// COUNTS. MGP_CALL_LIST_DOCUMENTED_COUNT below is the authority; PipeCatalogueTest asserts
|
||||
// that the expansion, the two generated tables and this number agree.
|
||||
//
|
||||
// class entries group (as the plan tabulates it)
|
||||
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||
// appended server-side fence wait 1
|
||||
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
||||
// kCtxCso 13 CSO create/bind/delete
|
||||
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
|
||||
// kCtxObject 9 set_texture_params (the 17th set_*) + 8 object-scoped transfers
|
||||
// kCtxVerb 13 3 context-reading transfer calls + the 10 commands
|
||||
// total 71
|
||||
//
|
||||
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
||||
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
||||
// - "screen 14" tabulates the fence and query families together with the screen block.
|
||||
// Section 4.3 assigns the query NAMESPACE to the context ("VAO / FBO / XFB object /
|
||||
// query namespaces, the command stream, present"), so the six query calls carry
|
||||
// kCtxQuery and live in MGPipeContext. Screen keeps 10 of the plan's (11 with the appended
|
||||
// FenceWaitServer, below). The eight EGL lifecycle entry points stay virtual functions on
|
||||
// pActiveBackendObject and are deliberately NOT calls here (section 4.4.1, last row).
|
||||
// - "CSO 15" is create/bind/delete x 5 kinds. Two of those binds are ALSO named in the
|
||||
// set_* catalogue as their array forms - bind_sampler_states and set_sampler_views
|
||||
// (section 4.4.3) - and a call may only exist once, so they are emitted under
|
||||
// kCtxState and the CSO group holds 13: create/delete x 5 plus the three remaining
|
||||
// binds (render state, vertex elements, shader).
|
||||
// - "transfer 12" enumerates 11 calls in section 4.4.4 plus appendix A
|
||||
// (resource_subdata, buffer_subdata_resident, resource_flush_range, resource_readback,
|
||||
// resource_copy_region, blit, clear, generate_mipmap, read_pixels, get_texture_image,
|
||||
// resource_subdata_complete). Eleven is what is emitted; the twelfth is not named
|
||||
// anywhere in the plan.
|
||||
// - "about 74 items" in section 4.1 is the sum of those headline numbers, so it inherits
|
||||
// the same double counting. 68 unique records was the honest total of the plan's own
|
||||
// catalogue.
|
||||
// - Three LIVE GLFunctionsTable entries had no carrier in it at all: GetGpuTimestampNs
|
||||
// (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp
|
||||
// (glQueryCounter, a one-shot stamp rather than a begin/end pair) and WaitSync (the
|
||||
// GPU-side wait, which FenceWait's client-side wait does not express). They are
|
||||
// QueryTimestamp, QueryCounter and FenceWaitServer, APPENDED at the end of the list -
|
||||
// not slotted into their groups - because the wire opcode is the position, so a record
|
||||
// that arrives late goes last. 71 unique records.
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
#define MGP_CALL_LIST_DOCUMENTED_COUNT 71
|
||||
|
||||
// clang-format off
|
||||
#define MGP_CALL_LIST(X) \
|
||||
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
|
||||
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
|
||||
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
|
||||
X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \
|
||||
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
|
||||
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
|
||||
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
|
||||
X(FenceCreate, MGPHandleOnly, kScreen, kNone) \
|
||||
X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot) \
|
||||
X(FenceWait, MGPFenceWait, kScreen, kReplySlot) \
|
||||
X(FenceDestroy, MGPHandleOnly, kScreen, kNone) \
|
||||
/* ---- context: query objects (plan 4.3 gives the namespace to the context) ---- */ \
|
||||
X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot) \
|
||||
X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot) \
|
||||
X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone) \
|
||||
/* ---- context: CSO create/bind/delete (plan 4.4.2, 4.5.2-4.5.5) ---- */ \
|
||||
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \
|
||||
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \
|
||||
X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
|
||||
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kNone) \
|
||||
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
|
||||
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob) \
|
||||
X(BindShaderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
/* ---- context: set_* (plan 4.4.3) ---- */ \
|
||||
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \
|
||||
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \
|
||||
X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail) \
|
||||
X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone) \
|
||||
X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone) \
|
||||
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \
|
||||
X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail) \
|
||||
X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail) \
|
||||
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \
|
||||
X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail) \
|
||||
X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob) \
|
||||
X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail) \
|
||||
X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone) \
|
||||
X(SetPatchState, MGPPatchState, kCtxState, kNone) \
|
||||
X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone) \
|
||||
X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone) \
|
||||
/* Migration-only carrier for Track V, retired field by field across P2..P13. Its */ \
|
||||
/* retirement is a compile error: MGL_RESIDUAL_BLOCK_SIZE only ever goes DOWN and the */ \
|
||||
/* final step asserts sizeof(ResidualValueBlock) == 0 (plan 6.3). */ \
|
||||
X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) \
|
||||
/* ---- context: per-object state and transfer (plan 4.4.3 set_texture_params, 4.4.4) ---- */ \
|
||||
X(SetTextureParams, MGPTextureParams, kCtxObject, kNone) \
|
||||
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
|
||||
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
|
||||
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
|
||||
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
|
||||
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
|
||||
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
|
||||
X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone) \
|
||||
X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot) \
|
||||
/* ---- context: transfer calls that read whole-context state, and the commands ---- */ \
|
||||
X(Blit, MGPBlit, kCtxVerb, kNone) \
|
||||
X(Clear, MGPClear, kCtxVerb, kNone) \
|
||||
X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot) \
|
||||
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \
|
||||
X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone) \
|
||||
X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone) \
|
||||
X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone) \
|
||||
X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone) \
|
||||
X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
|
||||
X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
|
||||
X(Flush, MGPFlush, kCtxVerb, kNone) \
|
||||
X(Present, MGPPresent, kCtxVerb, kNone) \
|
||||
X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) \
|
||||
/* ---- APPENDED. Opcodes are positional, so a late arrival goes at the END, never into ---- */ \
|
||||
/* ---- its group: three live GLFunctionsTable entries the catalogue had no carrier for. ---- */ \
|
||||
/* glGetInteger64v(GL_TIMESTAMP) - GetGpuTimestampNs, a synchronous server answer, which */ \
|
||||
/* the reply slot carries. The query namespace is the context's (plan 4.3). */ \
|
||||
X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot) \
|
||||
/* glQueryCounter(GL_TIMESTAMP) - QueryCounterTimestamp, a one-shot stamp into a query */ \
|
||||
/* object, NOT a begin/end pair. Kind carries GL_TIMESTAMP. */ \
|
||||
X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
/* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \
|
||||
/* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \
|
||||
X(FenceWaitServer, MGPFenceWait, kScreen, kNone)
|
||||
// clang-format on
|
||||
|
||||
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
||||
// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry -
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each,
|
||||
// the only indexed pnames the device rather than the frontend answers - live in MGPCaps
|
||||
// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled
|
||||
// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from
|
||||
// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names
|
||||
// frontend state and is answered before any table is consulted.
|
||||
// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact
|
||||
// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not
|
||||
// a backend answer at all; nothing a backend knows about a program crosses this way.
|
||||
// - ShaderStorageBlockBinding (folded into MGPProgramDesc's reflection archive),
|
||||
// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a
|
||||
// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views
|
||||
// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3).
|
||||
@@ -0,0 +1,313 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeFields.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
|
||||
|
||||
// Field lists for the G4 shadow comparator (plan B section 10.3-2). One macro per payload
|
||||
// in MGPipeTypes.h, listing the fields that carry MEANING - padding is deliberately absent,
|
||||
// because MOBILEGL_PIPE_VERIFY has to have ZERO false positives and a padding byte is
|
||||
// exactly what makes a memcmp of RenderStateParameters false-DIFFER
|
||||
// (DirectGLES.cpp documents that behaviour where it does the same comparison itself).
|
||||
//
|
||||
// Hand maintained alongside MGPipeTypes.h, MGPipeValueTypes.h, MGPipeHostSpan.h and
|
||||
// MG_Backend/BackendObject.h. Adding a member to one of these structs without adding it here
|
||||
// would make the comparator blind to it, so gen_pipe.py asserts - in both modes, hence in
|
||||
// pipe-gates - that every list below names exactly the direct data members of its struct
|
||||
// (P1 brief D8; a member named Pad<n> is padding and is not listed).
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
#define MGP_FIELDS_MGPBlobRef(F) \
|
||||
F(Offset) F(Size) F(Seg)
|
||||
|
||||
#define MGP_FIELDS_MGPRange(F) \
|
||||
F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPBox(F) \
|
||||
F(X) F(Y) F(Z) F(W) F(H) F(D)
|
||||
|
||||
#define MGP_FIELDS_MGPReplySlot(F) \
|
||||
F(Id)
|
||||
|
||||
#define MGP_FIELDS_MGPStateChunk(F) \
|
||||
F(Offset) F(Length)
|
||||
|
||||
#define MGP_FIELDS_MGPHandleOnly(F) \
|
||||
F(Handle) F(Kind)
|
||||
|
||||
#define MGP_FIELDS_MGPCaps(F) \
|
||||
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
|
||||
|
||||
#define MGP_FIELDS_MGPResourceDesc(F) \
|
||||
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
|
||||
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
|
||||
F(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
|
||||
F(BufOffset) F(BufSize)
|
||||
|
||||
#define MGP_FIELDS_MGPFenceWait(F) \
|
||||
F(Fence) F(TimeoutNs)
|
||||
|
||||
#define MGP_FIELDS_MGPQueryDesc(F) \
|
||||
F(Query) F(Kind) F(Stream)
|
||||
|
||||
#define MGP_FIELDS_MGPQueryResultRequest(F) \
|
||||
F(Query) F(Wait)
|
||||
|
||||
#define MGP_FIELDS_MGPTimestampRequest(F) \
|
||||
F(Reserved)
|
||||
|
||||
#define MGP_FIELDS_MGPRenderStateDesc(F) \
|
||||
F(Cso) F(BaseCso) F(ChunkMask) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPBindRenderState(F) \
|
||||
F(Cso) F(Version) F(PipelineVersion)
|
||||
|
||||
#define MGP_FIELDS_MGPDynamicState(F) \
|
||||
F(ChunkMask) F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexElements(F) \
|
||||
F(Cso) F(AttributeCount) F(BindingPointCount) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerDesc(F) \
|
||||
F(Cso) F(Parameters)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerView(F) \
|
||||
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
||||
F(Samples) F(FixedSampleLocations)
|
||||
|
||||
#define MGP_FIELDS_MGPTextureParams(F) \
|
||||
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) F(MinLod) F(MaxLod) \
|
||||
F(LodBias)
|
||||
|
||||
#define MGP_FIELDS_MGPProgramDesc(F) \
|
||||
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
||||
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
|
||||
|
||||
#define MGP_FIELDS_MGPSurface(F) \
|
||||
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
|
||||
|
||||
#define MGP_FIELDS_MGPFramebufferState(F) \
|
||||
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
|
||||
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
||||
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexBuffers(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPIndexBuffer(F) \
|
||||
F(Res) F(Offset) F(IndexSize)
|
||||
|
||||
#define MGP_FIELDS_MGPIndirectBuffers(F) \
|
||||
F(DrawIndirect) F(Parameter)
|
||||
|
||||
#define MGP_FIELDS_MGPBoundView(F) \
|
||||
F(View) F(Texture) F(Unit)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerViews(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerStates(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPImageView(F) \
|
||||
F(Res) F(Unit) F(InternalFormat) F(Layer) F(Level) F(Layered) F(Access)
|
||||
|
||||
#define MGP_FIELDS_MGPShaderImages(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPBufferRange(F) \
|
||||
F(Res) F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPShaderBuffers(F) \
|
||||
F(Class) F(Start) F(Count) F(WritableMask) F(HostSpanCount) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputTargets(F) \
|
||||
F(Count) F(Generation) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPGlobalConstants(F) \
|
||||
F(ShaderCso) F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPAttribValue(F) \
|
||||
F(Location) F(ValueClass) F(Data)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexAttribDefaults(F) \
|
||||
F(Mask) F(Count)
|
||||
|
||||
#define MGP_FIELDS_MGPPixelPackState(F) \
|
||||
F(Pack)
|
||||
|
||||
#define MGP_FIELDS_MGPPatchState(F) \
|
||||
F(Vertices) F(Outer) F(Inner)
|
||||
|
||||
#define MGP_FIELDS_ResidualValueBlock(F) \
|
||||
F(RenderState) F(Pack) F(CapabilityBits) F(PatchVertices) F(PatchOuter) F(PatchInner)
|
||||
|
||||
#define MGP_FIELDS_MGPResidualValueState(F) \
|
||||
F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSubRegion(F) \
|
||||
F(X) F(Y) F(Z) F(W) F(H) F(D) F(SrcOffset) F(SrcRowStride) F(SrcSliceStride)
|
||||
|
||||
#define MGP_FIELDS_MGPSubData(F) \
|
||||
F(Res) F(Target) F(Level) F(SourceIsVerbatimLevelShadow) F(UnionBox) F(RegionCount) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSubDataComplete(F) \
|
||||
F(Res) F(Target) F(FirstLevel) F(LevelCount) F(PullSerial)
|
||||
|
||||
#define MGP_FIELDS_MGPFlushRange(F) \
|
||||
F(Res) F(Offset) F(Size) F(AccessFlags)
|
||||
|
||||
#define MGP_FIELDS_MGPReadback(F) \
|
||||
F(Res) F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPCopyRegion(F) \
|
||||
F(Src) F(Dst) F(SrcBox) F(DstX) F(DstY) F(DstZ) F(SrcTarget) F(DstTarget) F(SrcLevel) F(DstLevel)
|
||||
|
||||
#define MGP_FIELDS_MGPBlit(F) \
|
||||
F(ReadFbo) F(DrawFbo) F(SrcX0) F(SrcY0) F(SrcX1) F(SrcY1) F(DstX0) F(DstY0) F(DstX1) F(DstY1) \
|
||||
F(Mask) F(Filter)
|
||||
|
||||
#define MGP_FIELDS_MGPClear(F) \
|
||||
F(Fbo) F(Kind) F(DrawBufferIndex) F(BufferMask) F(ValueClass) F(ColorValue) F(DepthValue) \
|
||||
F(StencilValue)
|
||||
|
||||
#define MGP_FIELDS_MGPMipPlan(F) \
|
||||
F(Res) F(Target) F(BaseLevel) F(LevelCount)
|
||||
|
||||
#define MGP_FIELDS_MGPReadbackInfo(F) \
|
||||
F(Res) F(Box) F(Format) F(Type) F(Target) F(Level) F(DstOffset) F(DstSize)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawInfo(F) \
|
||||
F(Mode) F(IndexSize) F(Flags) F(InstanceCount) F(StartInstance) F(RestartIndex) F(DrawIdOffset) \
|
||||
F(IndexResource) F(MinIndex) F(MaxIndex) F(XfbCpuCapturedVertices) F(NumDraws)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawRange(F) \
|
||||
F(Start) F(Count) F(IndexBias)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawIndirect(F) \
|
||||
F(Buffer) F(ParameterBuffer) F(Offset) F(ParameterOffset) F(Stride) F(DrawCount)
|
||||
|
||||
#define MGP_FIELDS_MGPGridInfo(F) \
|
||||
F(GridX) F(GridY) F(GridZ) F(BlockX) F(BlockY) F(BlockZ) F(IndirectBuffer) F(IndirectOffset) \
|
||||
F(IsIndirect)
|
||||
|
||||
#define MGP_FIELDS_MGPMemoryBarrier(F) \
|
||||
F(Bits) F(ByRegion)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputBegin(F) \
|
||||
F(PrimitiveMode)
|
||||
|
||||
#define MGP_FIELDS_MGPXfbAccounting(F) \
|
||||
F(CapturedVertices) F(PrimitivesWritten) F(PrimitiveMode)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputControl(F) \
|
||||
F(Reserved)
|
||||
|
||||
#define MGP_FIELDS_MGPFlush(F) \
|
||||
F(Flags)
|
||||
|
||||
#define MGP_FIELDS_MGPPresent(F) \
|
||||
F(FrameSerial)
|
||||
|
||||
#define MGP_FIELDS_MGPSwapInterval(F) \
|
||||
F(Interval)
|
||||
|
||||
#define MGP_FIELDS_MGPSurfaceInfo(F) \
|
||||
F(Width) F(Height) F(InternalFormat) F(Samples) F(Layers) F(IsDefault)
|
||||
|
||||
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
|
||||
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
|
||||
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
|
||||
// gone (a struct without a list is a compile error), and gen_pipe.py asserts every list names
|
||||
// every direct data member of its struct - Pad-named members are padding and excluded - so a
|
||||
// member added to RenderStateParameters without a row here fails pipe-gates.
|
||||
|
||||
#define MGP_FIELDS_RenderStateParameters(F) \
|
||||
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
|
||||
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
|
||||
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
|
||||
F(DepthMask) F(ColorMasks) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
|
||||
F(DepthRanges) F(SampleCoverageValue) F(SampleCoverageInvert) F(SampleMaskValue) \
|
||||
F(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
|
||||
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
|
||||
F(TextureCompressionHint) F(FragmentShaderDerivativeHint) F(PointFadeThresholdSize) \
|
||||
F(PointSpriteCoordOrigin) F(ClampReadColor) F(PolygonModeFront) F(PolygonModeBack) \
|
||||
F(PrimitiveRestartIndex) F(ColorLogicOpEnabled) F(DebugOutputEnabled) \
|
||||
F(DebugOutputSynchronousEnabled) F(DitherEnabled) F(LineSmoothEnabled) F(MultisampleEnabled) \
|
||||
F(PolygonOffsetFillEnabled) F(PolygonOffsetLineEnabled) F(PolygonOffsetPointEnabled) \
|
||||
F(PolygonSmoothEnabled) F(PrimitiveRestartEnabled) F(PrimitiveRestartFixedIndexEnabled) \
|
||||
F(RasterizerDiscardEnabled) F(SampleAlphaToCoverageEnabled) F(SampleAlphaToOneEnabled) \
|
||||
F(SampleCoverageEnabled) F(SampleMaskEnabled) F(SampleShadingEnabled) F(StencilTestEnabled) \
|
||||
F(ProgramPointSizeEnabled) F(ScissorTestEnabledMask) F(ScissorBoxes) F(ScissorBoxWrittenMask) \
|
||||
F(ClipDistanceEnabledMask)
|
||||
|
||||
#define MGP_FIELDS_PixelStoreParameters(F) \
|
||||
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
||||
F(Alignment)
|
||||
|
||||
#define MGP_FIELDS_PerBufferBlendState(F) \
|
||||
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
||||
F(AlphaEquation)
|
||||
|
||||
#define MGP_FIELDS_StencilFaceState(F) \
|
||||
F(Func) F(Ref) F(ValueMask) F(WriteMask) F(FailOp) F(PassDepthFailOp) F(PassDepthPassOp)
|
||||
|
||||
#define MGP_FIELDS_DynamicBackendParameters(F) \
|
||||
F(UniformBufferOffsetAlignment) F(ShaderStorageBufferOffsetAlignment) F(MaxTextureMaxAnisotropy) \
|
||||
F(AliasedLineWidthRangeMin) F(AliasedLineWidthRangeMax) F(SmoothLineWidthRangeMin) \
|
||||
F(SmoothLineWidthRangeMax) F(SmoothLineWidthGranularity) F(PointSizeRangeMin) \
|
||||
F(PointSizeRangeMax) F(PointSizeGranularity) F(Max3DTextureSize) F(MaxArrayTextureLayers) \
|
||||
F(MaxCubeMapTextureSize) F(MaxFramebufferWidth) F(MaxFramebufferHeight) F(MaxFramebufferLayers) \
|
||||
F(MaxRenderbufferSize) F(MaxTextureSize) F(MaxColorTextureSamples) F(MaxDepthTextureSamples) \
|
||||
F(MaxFramebufferSamples) F(MaxIntegerSamples) F(MaxSamples) F(MaxSampleMaskWords) \
|
||||
F(MaxPatchVertices) F(MaxTessGenLevel) F(MinProgramTextureGatherOffset) \
|
||||
F(MaxProgramTextureGatherOffset) F(MaxTextureImageUnits) F(MaxVertexTextureImageUnits) \
|
||||
F(MaxComputeTextureImageUnits) F(MaxCombinedTextureImageUnits) F(MaxVertexAttribs) \
|
||||
F(MaxComputeShaderStorageBlocks) F(MaxCombinedShaderStorageBlocks) \
|
||||
F(MaxVertexShaderStorageBlocks) F(MaxTessControlShaderStorageBlocks) \
|
||||
F(MaxTessEvaluationShaderStorageBlocks) F(MaxGeometryShaderStorageBlocks) \
|
||||
F(MaxFragmentShaderStorageBlocks) F(MaxComputeUniformBlocks) F(MaxComputeWorkGroupInvocations) \
|
||||
F(MaxComputeWorkGroupCount) F(MaxComputeWorkGroupSize) F(MaxShaderStorageBufferBindings) \
|
||||
F(MaxTextureBufferSize) F(TextureBufferOffsetAlignment) F(MaxUniformBufferBindings) \
|
||||
F(MaxUniformBlockSize) F(MaxImageUnits) F(MaxCombinedImageUniforms) F(MaxVertexImageUniforms) \
|
||||
F(MaxGeometryImageUniforms) F(MaxFragmentImageUniforms) F(MaxComputeImageUniforms) \
|
||||
F(MaxDrawBuffers) F(MaxColorAttachments) F(MaxClipDistances) F(MaxCullDistances) \
|
||||
F(MaxCombinedClipAndCullDistances) F(MaxViewports) F(LayerProvokingVertex) \
|
||||
F(ViewportIndexProvokingVertex) F(MaxViewportWidth) F(MaxViewportHeight) \
|
||||
F(ViewportBoundsRangeMin) F(ViewportBoundsRangeMax) F(ViewportSubpixelBits) \
|
||||
F(MinFragmentInterpolationOffset) F(MaxFragmentInterpolationOffset) \
|
||||
F(FragmentInterpolationOffsetBits) F(SupportsWideLines) \
|
||||
F(SupportsDistinctDepthStencilAttachments) F(PerLayerFramebufferAttachmentTargets) \
|
||||
F(SupportsShaderFloat64) F(SupportsFloat64VertexAttributes) F(SupportsTessellationPointSize) \
|
||||
F(SupportsGeometryPointSize) F(MaxShaderStorageBlockSize) F(SubgroupSize) \
|
||||
F(SubgroupSupportedStages) F(SubgroupSupportedFeatures) F(SubgroupQuadOperationsInAllStages) \
|
||||
F(GpuVendor)
|
||||
|
||||
#define MGP_FIELDS_MGHostSpan(F) \
|
||||
F(Ptr) F(Seg) F(Size) F(Offset)
|
||||
|
||||
// Every payload above, in the order the comparator is generated. Keep in sync with the
|
||||
// macros; gen_pipe.py reads THIS list to know what to emit.
|
||||
#define MGP_VERIFY_PAYLOAD_LIST(P) \
|
||||
P(MGPBlobRef) P(MGPRange) P(MGPBox) P(MGPReplySlot) P(MGPStateChunk) P(MGPHandleOnly) P(MGPCaps) \
|
||||
P(MGPResourceDesc) P(MGPFenceWait) P(MGPQueryDesc) P(MGPQueryResultRequest) P(MGPTimestampRequest) P(MGPRenderStateDesc) \
|
||||
P(MGPBindRenderState) P(MGPDynamicState) P(MGPVertexElements) P(MGPSamplerDesc) P(MGPSamplerView) \
|
||||
P(MGPTextureParams) P(MGPProgramDesc) P(MGPSurface) P(MGPFramebufferState) P(MGPVertexBuffer) \
|
||||
P(MGPVertexBuffers) P(MGPIndexBuffer) P(MGPIndirectBuffers) P(MGPBoundView) P(MGPSamplerViews) \
|
||||
P(MGPSamplerStates) P(MGPImageView) P(MGPShaderImages) P(MGPBufferRange) P(MGPShaderBuffers) \
|
||||
P(MGPStreamOutputTargets) P(MGPGlobalConstants) P(MGPAttribValue) P(MGPVertexAttribDefaults) \
|
||||
P(MGPPixelPackState) P(MGPPatchState) P(ResidualValueBlock) P(MGPResidualValueState) \
|
||||
P(MGPSubRegion) P(MGPSubData) P(MGPSubDataComplete) P(MGPFlushRange) P(MGPReadback) \
|
||||
P(MGPCopyRegion) P(MGPBlit) P(MGPClear) P(MGPMipPlan) P(MGPReadbackInfo) P(MGPDrawInfo) \
|
||||
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
||||
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
||||
P(MGPSurfaceInfo) \
|
||||
P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \
|
||||
P(DynamicBackendParameters) P(MGHostSpan)
|
||||
|
||||
// clang-format on
|
||||
@@ -0,0 +1,28 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeInputsSwitch.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
|
||||
#ifndef MOBILEGL_MG_PIPE_INPUTS_SWITCH_H // belt and braces: reachable as <MG_Pipe/..> and <..> (CMakeLists.txt:531,535)
|
||||
#define MOBILEGL_MG_PIPE_INPUTS_SWITCH_H
|
||||
// The strangler switch (ARCHITECTURE.md 9.2). Every backend read of frontend state is spelled
|
||||
// MGB_CTX->Accessor(...). Pull arm: the live GLContext, so the pull build is the tree before P1
|
||||
// token for token. Push arm: the PipeInputs block the frontend fills at every verb boundary.
|
||||
// The pull arm is the ONLY place under MobileGL/ outside MG_State and MG_Impl that may spell
|
||||
// pGLContext; purity gate C greps MG_Backend/ for that token.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
|
||||
#define MGB_CTX_LIVE (::MobileGL::MG_Pipe::gPipeInputs.IsLive())
|
||||
#define MGB_CTX_IDENTITY (::MobileGL::MG_Pipe::gPipeInputs.ContextIdentity())
|
||||
#else
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#define MGB_CTX (::MobileGL::MG_State::pGLContext)
|
||||
#define MGB_CTX_LIVE (::MobileGL::MG_State::pGLContext != nullptr)
|
||||
#define MGB_CTX_IDENTITY (static_cast<const void*>(::MobileGL::MG_State::pGLContext.get()))
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeMutation.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
|
||||
#ifndef MOBILEGL_MG_PIPE_MUTATION_H // belt and braces: reachable as <MG_Pipe/..> and <..>
|
||||
#define MOBILEGL_MG_PIPE_MUTATION_H
|
||||
// Push-on-mutation (P1 lane finding F2). MGP_FILL copies a verb's may-read set out of the
|
||||
// live GLContext at the verb boundary; the backend then reads that copy for the whole verb.
|
||||
// A backend that WRITES a frontend object inside its own verb - Magma synthesising a
|
||||
// fallback texture for an unbound sampler, materialising a queued clear, or overriding a
|
||||
// sampler's filter - moves a value the boundary already copied, and every read after that
|
||||
// point sees a block that no longer equals the live context. That is a real divergence, not
|
||||
// a harness artefact: the pull build reads the moved value and the push build does not.
|
||||
//
|
||||
// The frontend mutator that moves such a value spells MGP_NOTE_MUTATION(Field) right where
|
||||
// it moves it. The notice refreshes that ONE field in the pushed block when the field
|
||||
// belongs to the verb currently in flight, so "the pushed block equals the live context at
|
||||
// every read" stays literally true and the push build keeps pull semantics. It refreshes
|
||||
// the value only and never the poison stamp, so a withheld stamp (MOBILEGL_PIPE_POISON_OMIT,
|
||||
// negative control B) stays withheld.
|
||||
//
|
||||
// In the pull build the macro is ((void)0) and this header includes nothing, so the pull
|
||||
// build is byte-identical to a tree without it.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext).
|
||||
// A no-op unless a context is live, a verb has been filled, and `field` is in that verb
|
||||
// class's may-read mask; a forwarded (sticky) field has no storage and is never copied.
|
||||
void MGPipeNoteFrontendMutation(MGPipeInputField field);
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_NOTE_MUTATION(Field) \
|
||||
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
||||
#else
|
||||
#define MGP_NOTE_MUTATION(Field) ((void)0)
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeCoverage.inc
|
||||
// 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
|
||||
|
||||
// G6: backend read inventory -> MGPipe call coverage.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from Coverage.def and scripts/data/backend_read_inventory.md - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// The acceptance rule (plan B section 10.3-5): regenerate, `git diff --exit-code`, and
|
||||
// ZERO unmapped rows. P0 permits unmapped rows and only counts them; the count below is
|
||||
// the number the later gate has to drive to zero.
|
||||
//
|
||||
// Three pseudo-calls stand for read points that never become a forward record:
|
||||
// kClientResolved (the frontend answers it), kReverseChannel (it becomes one of the ten
|
||||
// MGPipeCallbacks) and kStructuralHandle (the row is a signature carrying a
|
||||
// SharedPtr<MG_State...> that becomes an MGPipeHandle parameter).
|
||||
|
||||
struct MGPipeCoverageEntry {
|
||||
const char* Accessor;
|
||||
const char* Call;
|
||||
Uint32 ReadPoints;
|
||||
};
|
||||
|
||||
inline constexpr MGPipeCoverageEntry kMGPipeCoverage[] = {
|
||||
{"Buffer ops delta", "ResourceRespecify", 17},
|
||||
{"GetActiveTextureUnit", "SetSamplerViews", 8},
|
||||
{"GetBlendColor", "SetDynamicState", 1},
|
||||
{"GetBlendEquationIndexed", "CreateRenderState", 1},
|
||||
{"GetBlendFuncIndexed", "CreateRenderState", 1},
|
||||
{"GetBoundTransformFeedbackName", "SetStreamOutputTargets", 1},
|
||||
{"GetBoundVertexArray", "BindVertexElements", 12},
|
||||
{"GetBufferBindingPoint", "SetShaderBuffers", 19},
|
||||
{"GetBufferBindingPointCount", "SetShaderBuffers", 3},
|
||||
{"GetBufferBindingSlot", "SetIndirectBuffers", 29},
|
||||
{"GetClampReadColor", "SetDynamicState", 1},
|
||||
{"GetClearColor", "SetDynamicState", 1},
|
||||
{"GetClearDepth", "SetDynamicState", 1},
|
||||
{"GetClearStencil", "SetDynamicState", 1},
|
||||
{"GetColorMaskIndexed", "CreateRenderState", 6},
|
||||
{"GetCullFaceMode", "CreateRenderState", 1},
|
||||
{"GetCurrentVertexAttribute", "SetVertexAttribDefaults", 2},
|
||||
{"GetDepthFunc", "CreateRenderState", 1},
|
||||
{"GetDepthMask", "CreateRenderState", 5},
|
||||
{"GetDepthRangeIndexed", "SetDynamicState", 1},
|
||||
{"GetFramebufferBindingSlot", "SetFramebufferState", 19},
|
||||
{"GetImageTextureBinding", "SetShaderImages", 14},
|
||||
{"GetLineWidth", "SetDynamicState", 1},
|
||||
{"GetLogicOp", "CreateRenderState", 1},
|
||||
{"GetMaxTouchedTextureUnit", "SetSamplerViews", 1},
|
||||
{"GetMinSampleShadingValue", "CreateRenderState", 1},
|
||||
{"GetPatchDefaultInnerLevel", "SetPatchState", 3},
|
||||
{"GetPatchDefaultOuterLevel", "SetPatchState", 3},
|
||||
{"GetPatchVertices", "SetPatchState", 3},
|
||||
{"GetPipelineStateVersion", "BindRenderState", 3},
|
||||
{"GetPixelStoreParameters", "SetPixelPackState", 6},
|
||||
{"GetPolygonModeFront", "CreateRenderState", 1},
|
||||
{"GetPolygonOffsetFactor", "SetDynamicState", 1},
|
||||
{"GetPolygonOffsetUnits", "SetDynamicState", 1},
|
||||
{"GetPrimitiveRestartIndex", "DrawVbo", 3},
|
||||
{"GetProgramForDispatch", "SetDispatchProgram", 3},
|
||||
{"GetProgramForDraw", "SetDrawProgram", 7},
|
||||
{"GetProgramObject", "CreateShaderState", 3},
|
||||
{"GetProvokingVertexMode", "CreateRenderState", 1},
|
||||
{"GetRenderStateParameters", "CreateRenderState", 11},
|
||||
{"GetRenderStateParametersVersion", "BindRenderState", 2},
|
||||
{"GetSamplingResolutionGeneration", "SetSamplerViews", 9},
|
||||
{"GetScissorBox", "SetDynamicState", 3},
|
||||
{"GetStencilState", "CreateRenderState", 8},
|
||||
{"GetTextureBindGeneration", "SetSamplerViews", 5},
|
||||
{"GetTextureContextId", "SetSamplerViews", 6},
|
||||
{"GetTextureObject", "SetSamplerViews", 1},
|
||||
{"GetTextureUnitObject", "SetSamplerViews", 19},
|
||||
{"GetTouchedBufferBindingPointCount", "SetShaderBuffers", 2},
|
||||
{"GetTransformFeedbackCapturedVertices", "DrawVbo", 1},
|
||||
{"GetTransformFeedbackGeneration", "SetStreamOutputTargets", 1},
|
||||
{"GetTransformFeedbackPausedPrimitiveCounter", "EndStreamOutput", 2},
|
||||
{"GetTransformFeedbackProgram", "SetStreamOutputTargets", 3},
|
||||
{"GetViewport", "SetDynamicState", 1},
|
||||
{"GetViewportIndexed", "SetDynamicState", 1},
|
||||
{"InvalidateCompileEnv", "kClientResolved", 2},
|
||||
{"IsCapabilityEnabled", "CreateRenderState", 29},
|
||||
{"IsCapabilityEnabledIndexed", "CreateRenderState", 1},
|
||||
{"IsTransformFeedbackActive", "BeginStreamOutput", 5},
|
||||
{"IsTransformFeedbackPaused", "PauseStreamOutput", 2},
|
||||
{"RecordError", "kReverseChannel", 6},
|
||||
{"ValidateProgramName", "kClientResolved", 3},
|
||||
{"handle-ify (wire handle)", "kStructuralHandle", 167},
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeCoverageEntryCount = 63;
|
||||
inline constexpr Uint32 kMGPipeInventoryReadPoints = 477;
|
||||
inline constexpr Uint32 kMGPipeInventoryMappedToCall = 299;
|
||||
inline constexpr Uint32 kMGPipeInventoryClientResolved = 5;
|
||||
inline constexpr Uint32 kMGPipeInventoryReverseChannel = 6;
|
||||
inline constexpr Uint32 kMGPipeInventoryStructuralHandle = 167;
|
||||
inline constexpr Uint32 kMGPipeInventoryUnmapped = 0;
|
||||
static_assert(kMGPipeCoverageEntryCount == sizeof(kMGPipeCoverage) / sizeof(kMGPipeCoverage[0]));
|
||||
static_assert(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
|
||||
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
|
||||
kMGPipeInventoryUnmapped ==
|
||||
kMGPipeInventoryReadPoints,
|
||||
"every inventory row must land in exactly one bucket");
|
||||
@@ -0,0 +1,300 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeFillPoints.inc
|
||||
// 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
|
||||
|
||||
// G5b: the verb enum, the verb classes and their may-read field masks.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from FillPoints.def, Coverage.def and MG_Backend/BackendObject.h - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// One verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
|
||||
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
|
||||
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
|
||||
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
|
||||
// read of any other field is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
|
||||
|
||||
enum class MGPipeVerb : Uint8 {
|
||||
DrawArrays,
|
||||
DrawElements,
|
||||
DrawElementsBaseVertex,
|
||||
MultiDrawArrays,
|
||||
MultiDrawElements,
|
||||
MultiDrawElementsBaseVertex,
|
||||
MultiDrawElementsIndirect,
|
||||
MultiDrawArraysIndirect,
|
||||
MultiDrawElementsIndirectCount,
|
||||
MultiDrawArraysIndirectCount,
|
||||
DrawRangeElementsBaseVertex,
|
||||
DrawRangeElements,
|
||||
DrawElementsInstancedBaseVertexBaseInstance,
|
||||
DrawElementsInstancedBaseVertex,
|
||||
DrawElementsInstancedBaseInstance,
|
||||
DrawElementsInstanced,
|
||||
DrawArraysInstancedBaseInstance,
|
||||
DrawArraysInstanced,
|
||||
DrawElementsIndirect,
|
||||
DrawArraysIndirect,
|
||||
Clear,
|
||||
ClearBufferfi,
|
||||
ClearBufferfv,
|
||||
ClearBufferuiv,
|
||||
ClearBufferiv,
|
||||
ClearNamedFramebufferfv,
|
||||
ClearNamedFramebufferfi,
|
||||
ClearNamedFramebufferiv,
|
||||
ClearNamedFramebufferuiv,
|
||||
BlitFramebuffer,
|
||||
BlitNamedFramebuffer,
|
||||
CopyTexImage2D,
|
||||
CopyTexSubImage2D,
|
||||
CopyImageSubData,
|
||||
GenerateMipmap,
|
||||
ReadPixels,
|
||||
GetTexImage,
|
||||
GetTextureImage,
|
||||
DispatchCompute,
|
||||
DispatchComputeIndirect,
|
||||
MemoryBarrier,
|
||||
MemoryBarrierByRegion,
|
||||
BindImageTexture,
|
||||
GetIntegeri_v,
|
||||
ShaderStorageBlockBinding,
|
||||
FenceSync,
|
||||
ClientWaitSync,
|
||||
WaitSync,
|
||||
DeleteSync,
|
||||
GetSyncStatus,
|
||||
IsTimerQuerySupported,
|
||||
BeginTimeElapsedQuery,
|
||||
EndTimeElapsedQuery,
|
||||
QueryCounterTimestamp,
|
||||
IsQueryResultAvailable,
|
||||
GetQueryResult64,
|
||||
DeleteBackendQuery,
|
||||
BeginOcclusionQuery,
|
||||
EndOcclusionQuery,
|
||||
BeginXfbPrimitivesQuery,
|
||||
EndXfbPrimitivesQuery,
|
||||
PatchParameteri,
|
||||
BeginTransformFeedback,
|
||||
EndTransformFeedback,
|
||||
PauseTransformFeedback,
|
||||
ResumeTransformFeedback,
|
||||
BindTransformFeedback,
|
||||
DeleteTransformFeedback,
|
||||
GetGpuTimestampNs,
|
||||
kVerbCount,
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);
|
||||
static_assert(kMGPipeVerbCount == 69, "the GLFunctionsTable verb set moved");
|
||||
|
||||
inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {
|
||||
"DrawArrays",
|
||||
"DrawElements",
|
||||
"DrawElementsBaseVertex",
|
||||
"MultiDrawArrays",
|
||||
"MultiDrawElements",
|
||||
"MultiDrawElementsBaseVertex",
|
||||
"MultiDrawElementsIndirect",
|
||||
"MultiDrawArraysIndirect",
|
||||
"MultiDrawElementsIndirectCount",
|
||||
"MultiDrawArraysIndirectCount",
|
||||
"DrawRangeElementsBaseVertex",
|
||||
"DrawRangeElements",
|
||||
"DrawElementsInstancedBaseVertexBaseInstance",
|
||||
"DrawElementsInstancedBaseVertex",
|
||||
"DrawElementsInstancedBaseInstance",
|
||||
"DrawElementsInstanced",
|
||||
"DrawArraysInstancedBaseInstance",
|
||||
"DrawArraysInstanced",
|
||||
"DrawElementsIndirect",
|
||||
"DrawArraysIndirect",
|
||||
"Clear",
|
||||
"ClearBufferfi",
|
||||
"ClearBufferfv",
|
||||
"ClearBufferuiv",
|
||||
"ClearBufferiv",
|
||||
"ClearNamedFramebufferfv",
|
||||
"ClearNamedFramebufferfi",
|
||||
"ClearNamedFramebufferiv",
|
||||
"ClearNamedFramebufferuiv",
|
||||
"BlitFramebuffer",
|
||||
"BlitNamedFramebuffer",
|
||||
"CopyTexImage2D",
|
||||
"CopyTexSubImage2D",
|
||||
"CopyImageSubData",
|
||||
"GenerateMipmap",
|
||||
"ReadPixels",
|
||||
"GetTexImage",
|
||||
"GetTextureImage",
|
||||
"DispatchCompute",
|
||||
"DispatchComputeIndirect",
|
||||
"MemoryBarrier",
|
||||
"MemoryBarrierByRegion",
|
||||
"BindImageTexture",
|
||||
"GetIntegeri_v",
|
||||
"ShaderStorageBlockBinding",
|
||||
"FenceSync",
|
||||
"ClientWaitSync",
|
||||
"WaitSync",
|
||||
"DeleteSync",
|
||||
"GetSyncStatus",
|
||||
"IsTimerQuerySupported",
|
||||
"BeginTimeElapsedQuery",
|
||||
"EndTimeElapsedQuery",
|
||||
"QueryCounterTimestamp",
|
||||
"IsQueryResultAvailable",
|
||||
"GetQueryResult64",
|
||||
"DeleteBackendQuery",
|
||||
"BeginOcclusionQuery",
|
||||
"EndOcclusionQuery",
|
||||
"BeginXfbPrimitivesQuery",
|
||||
"EndXfbPrimitivesQuery",
|
||||
"PatchParameteri",
|
||||
"BeginTransformFeedback",
|
||||
"EndTransformFeedback",
|
||||
"PauseTransformFeedback",
|
||||
"ResumeTransformFeedback",
|
||||
"BindTransformFeedback",
|
||||
"DeleteTransformFeedback",
|
||||
"GetGpuTimestampNs",
|
||||
};
|
||||
|
||||
enum class MGPipeVerbClass : Uint8 {
|
||||
kDraw,
|
||||
kDispatch,
|
||||
kClear,
|
||||
kBlitOrCopy,
|
||||
kTextureOp,
|
||||
kReadback,
|
||||
kXfbSpan,
|
||||
kProgramOp,
|
||||
kQuery,
|
||||
kClassCount,
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);
|
||||
static_assert(kMGPipeVerbClassCount == 9, "the verb class set moved");
|
||||
|
||||
inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {
|
||||
"kDraw",
|
||||
"kDispatch",
|
||||
"kClear",
|
||||
"kBlitOrCopy",
|
||||
"kTextureOp",
|
||||
"kReadback",
|
||||
"kXfbSpan",
|
||||
"kProgramOp",
|
||||
"kQuery",
|
||||
};
|
||||
|
||||
inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {
|
||||
MGPipeVerbClass::kDraw, // DrawArrays
|
||||
MGPipeVerbClass::kDraw, // DrawElements
|
||||
MGPipeVerbClass::kDraw, // DrawElementsBaseVertex
|
||||
MGPipeVerbClass::kDraw, // MultiDrawArrays
|
||||
MGPipeVerbClass::kDraw, // MultiDrawElements
|
||||
MGPipeVerbClass::kDraw, // MultiDrawElementsBaseVertex
|
||||
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirect
|
||||
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirect
|
||||
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirectCount
|
||||
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirectCount
|
||||
MGPipeVerbClass::kDraw, // DrawRangeElementsBaseVertex
|
||||
MGPipeVerbClass::kDraw, // DrawRangeElements
|
||||
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertexBaseInstance
|
||||
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertex
|
||||
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseInstance
|
||||
MGPipeVerbClass::kDraw, // DrawElementsInstanced
|
||||
MGPipeVerbClass::kDraw, // DrawArraysInstancedBaseInstance
|
||||
MGPipeVerbClass::kDraw, // DrawArraysInstanced
|
||||
MGPipeVerbClass::kDraw, // DrawElementsIndirect
|
||||
MGPipeVerbClass::kDraw, // DrawArraysIndirect
|
||||
MGPipeVerbClass::kClear, // Clear
|
||||
MGPipeVerbClass::kClear, // ClearBufferfi
|
||||
MGPipeVerbClass::kClear, // ClearBufferfv
|
||||
MGPipeVerbClass::kClear, // ClearBufferuiv
|
||||
MGPipeVerbClass::kClear, // ClearBufferiv
|
||||
MGPipeVerbClass::kClear, // ClearNamedFramebufferfv
|
||||
MGPipeVerbClass::kClear, // ClearNamedFramebufferfi
|
||||
MGPipeVerbClass::kClear, // ClearNamedFramebufferiv
|
||||
MGPipeVerbClass::kClear, // ClearNamedFramebufferuiv
|
||||
MGPipeVerbClass::kBlitOrCopy, // BlitFramebuffer
|
||||
MGPipeVerbClass::kBlitOrCopy, // BlitNamedFramebuffer
|
||||
MGPipeVerbClass::kBlitOrCopy, // CopyTexImage2D
|
||||
MGPipeVerbClass::kBlitOrCopy, // CopyTexSubImage2D
|
||||
MGPipeVerbClass::kBlitOrCopy, // CopyImageSubData
|
||||
MGPipeVerbClass::kTextureOp, // GenerateMipmap
|
||||
MGPipeVerbClass::kReadback, // ReadPixels
|
||||
MGPipeVerbClass::kReadback, // GetTexImage
|
||||
MGPipeVerbClass::kReadback, // GetTextureImage
|
||||
MGPipeVerbClass::kDispatch, // DispatchCompute
|
||||
MGPipeVerbClass::kDispatch, // DispatchComputeIndirect
|
||||
MGPipeVerbClass::kQuery, // MemoryBarrier
|
||||
MGPipeVerbClass::kQuery, // MemoryBarrierByRegion
|
||||
MGPipeVerbClass::kTextureOp, // BindImageTexture
|
||||
MGPipeVerbClass::kQuery, // GetIntegeri_v
|
||||
MGPipeVerbClass::kProgramOp, // ShaderStorageBlockBinding
|
||||
MGPipeVerbClass::kQuery, // FenceSync
|
||||
MGPipeVerbClass::kQuery, // ClientWaitSync
|
||||
MGPipeVerbClass::kQuery, // WaitSync
|
||||
MGPipeVerbClass::kQuery, // DeleteSync
|
||||
MGPipeVerbClass::kQuery, // GetSyncStatus
|
||||
MGPipeVerbClass::kQuery, // IsTimerQuerySupported
|
||||
MGPipeVerbClass::kQuery, // BeginTimeElapsedQuery
|
||||
MGPipeVerbClass::kQuery, // EndTimeElapsedQuery
|
||||
MGPipeVerbClass::kQuery, // QueryCounterTimestamp
|
||||
MGPipeVerbClass::kQuery, // IsQueryResultAvailable
|
||||
MGPipeVerbClass::kQuery, // GetQueryResult64
|
||||
MGPipeVerbClass::kQuery, // DeleteBackendQuery
|
||||
MGPipeVerbClass::kQuery, // BeginOcclusionQuery
|
||||
MGPipeVerbClass::kQuery, // EndOcclusionQuery
|
||||
MGPipeVerbClass::kQuery, // BeginXfbPrimitivesQuery
|
||||
MGPipeVerbClass::kQuery, // EndXfbPrimitivesQuery
|
||||
MGPipeVerbClass::kQuery, // PatchParameteri
|
||||
MGPipeVerbClass::kXfbSpan, // BeginTransformFeedback
|
||||
MGPipeVerbClass::kXfbSpan, // EndTransformFeedback
|
||||
MGPipeVerbClass::kXfbSpan, // PauseTransformFeedback
|
||||
MGPipeVerbClass::kXfbSpan, // ResumeTransformFeedback
|
||||
MGPipeVerbClass::kXfbSpan, // BindTransformFeedback
|
||||
MGPipeVerbClass::kXfbSpan, // DeleteTransformFeedback
|
||||
MGPipeVerbClass::kQuery, // GetGpuTimestampNs
|
||||
};
|
||||
|
||||
// One bit per MGPipeInputField. The 7 sticky fields are OR'ed into every class.
|
||||
struct MGPipeFieldMask {
|
||||
Uint64 Words[2];
|
||||
};
|
||||
|
||||
inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {
|
||||
const SizeT index = static_cast<SizeT>(field);
|
||||
return (mask.Words[index / 64] >> (index % 64)) & 1u;
|
||||
}
|
||||
|
||||
inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {
|
||||
// kDraw: 54 fields (47 own + 7 sticky)
|
||||
{{0x7ffbfff7bfffc3eeull, 0x0000000000000000ull}},
|
||||
// kDispatch: 22 fields (15 own + 7 sticky)
|
||||
{{0x5c40f2281d3003c0ull, 0x0000000000000000ull}},
|
||||
// kClear: 25 fields (18 own + 7 sticky)
|
||||
{{0x5c50ffa001347900ull, 0x0000000000000000ull}},
|
||||
// kBlitOrCopy: 29 fields (22 own + 7 sticky)
|
||||
{{0x5f70ffe0013c4181ull, 0x0000000000000000ull}},
|
||||
// kTextureOp: 17 fields (10 own + 7 sticky)
|
||||
{{0x5c40f22001300181ull, 0x0000000000000000ull}},
|
||||
// kReadback: 24 fields (17 own + 7 sticky)
|
||||
{{0x5f50f3a041300541ull, 0x0000000000000000ull}},
|
||||
// kXfbSpan: 15 fields (8 own + 7 sticky)
|
||||
{{0x7f0b402000000380ull, 0x0000000000000000ull}},
|
||||
// kProgramOp: 18 fields (11 own + 7 sticky)
|
||||
{{0x5c50f3a001300100ull, 0x0000000000000000ull}},
|
||||
// kQuery: 8 fields (1 own + 7 sticky)
|
||||
{{0x5c04402000000100ull, 0x0000000000000000ull}},
|
||||
};
|
||||
|
||||
static_assert(kMGPipeInputFieldCount <= 2 * 64, "MGPipeFieldMask needs another word");
|
||||
@@ -0,0 +1,321 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeFilled.inc
|
||||
// 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
|
||||
|
||||
// G5: PipeInputs field ids and the per-verb poison generations.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from Coverage.def and PipeCalls.def - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// One field id per GLContext accessor the backends actually read (plan B section 6.2:
|
||||
// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is
|
||||
// small and stable across the whole migration).
|
||||
//
|
||||
// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case:
|
||||
// a field filled by the previous DRAW and then read by the glTexSubImage that follows is
|
||||
// stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a
|
||||
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
|
||||
// Fatal{UnmigratedPipeInput} (section 6.2.2).
|
||||
//
|
||||
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
|
||||
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
|
||||
|
||||
enum class MGPipeInputField : Uint16 {
|
||||
GetActiveTextureUnit,
|
||||
GetBlendColor,
|
||||
GetBlendEquationIndexed,
|
||||
GetBlendFuncIndexed,
|
||||
GetBoundTransformFeedbackName,
|
||||
GetBoundVertexArray,
|
||||
GetBufferBindingSlot,
|
||||
GetBufferBindingPoint,
|
||||
GetBufferBindingPointCount,
|
||||
GetTouchedBufferBindingPointCount,
|
||||
GetClampReadColor,
|
||||
GetClearColor,
|
||||
GetClearDepth,
|
||||
GetClearStencil,
|
||||
GetColorMaskIndexed,
|
||||
GetCullFaceMode,
|
||||
GetCurrentVertexAttribute,
|
||||
GetDepthFunc,
|
||||
GetDepthMask,
|
||||
GetDepthRangeIndexed,
|
||||
GetFramebufferBindingSlot,
|
||||
GetImageTextureBinding,
|
||||
GetLineWidth,
|
||||
GetLogicOp,
|
||||
GetMaxTouchedTextureUnit,
|
||||
GetMinSampleShadingValue,
|
||||
GetPatchDefaultInnerLevel,
|
||||
GetPatchDefaultOuterLevel,
|
||||
GetPatchVertices,
|
||||
GetPipelineStateVersion,
|
||||
GetPixelStoreParameters,
|
||||
GetPolygonModeFront,
|
||||
GetPolygonOffsetFactor,
|
||||
GetPolygonOffsetUnits,
|
||||
GetPrimitiveRestartIndex,
|
||||
GetProgramForDispatch,
|
||||
GetProgramForDraw,
|
||||
GetProgramObject,
|
||||
GetProvokingVertexMode,
|
||||
GetRenderStateParameters,
|
||||
GetRenderStateParametersVersion,
|
||||
GetSamplingResolutionGeneration,
|
||||
GetScissorBox,
|
||||
GetStencilState,
|
||||
GetTextureBindGeneration,
|
||||
GetTextureContextId,
|
||||
GetTextureObject,
|
||||
GetTextureUnitObject,
|
||||
GetTransformFeedbackCapturedVertices,
|
||||
GetTransformFeedbackGeneration,
|
||||
GetTransformFeedbackPausedPrimitiveCounter,
|
||||
GetTransformFeedbackProgram,
|
||||
GetViewport,
|
||||
GetViewportIndexed,
|
||||
IsCapabilityEnabled,
|
||||
IsCapabilityEnabledIndexed,
|
||||
IsTransformFeedbackActive,
|
||||
IsTransformFeedbackPaused,
|
||||
InvalidateCompileEnv,
|
||||
ValidateProgramName,
|
||||
RecordError,
|
||||
GetBoundTransformFeedbackLifetimeId,
|
||||
HasOpenTransformFeedbackSpan,
|
||||
kFieldCount,
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
|
||||
static_assert(kMGPipeInputFieldCount == 63, "the PipeInputs field set moved");
|
||||
|
||||
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
|
||||
"GetActiveTextureUnit",
|
||||
"GetBlendColor",
|
||||
"GetBlendEquationIndexed",
|
||||
"GetBlendFuncIndexed",
|
||||
"GetBoundTransformFeedbackName",
|
||||
"GetBoundVertexArray",
|
||||
"GetBufferBindingSlot",
|
||||
"GetBufferBindingPoint",
|
||||
"GetBufferBindingPointCount",
|
||||
"GetTouchedBufferBindingPointCount",
|
||||
"GetClampReadColor",
|
||||
"GetClearColor",
|
||||
"GetClearDepth",
|
||||
"GetClearStencil",
|
||||
"GetColorMaskIndexed",
|
||||
"GetCullFaceMode",
|
||||
"GetCurrentVertexAttribute",
|
||||
"GetDepthFunc",
|
||||
"GetDepthMask",
|
||||
"GetDepthRangeIndexed",
|
||||
"GetFramebufferBindingSlot",
|
||||
"GetImageTextureBinding",
|
||||
"GetLineWidth",
|
||||
"GetLogicOp",
|
||||
"GetMaxTouchedTextureUnit",
|
||||
"GetMinSampleShadingValue",
|
||||
"GetPatchDefaultInnerLevel",
|
||||
"GetPatchDefaultOuterLevel",
|
||||
"GetPatchVertices",
|
||||
"GetPipelineStateVersion",
|
||||
"GetPixelStoreParameters",
|
||||
"GetPolygonModeFront",
|
||||
"GetPolygonOffsetFactor",
|
||||
"GetPolygonOffsetUnits",
|
||||
"GetPrimitiveRestartIndex",
|
||||
"GetProgramForDispatch",
|
||||
"GetProgramForDraw",
|
||||
"GetProgramObject",
|
||||
"GetProvokingVertexMode",
|
||||
"GetRenderStateParameters",
|
||||
"GetRenderStateParametersVersion",
|
||||
"GetSamplingResolutionGeneration",
|
||||
"GetScissorBox",
|
||||
"GetStencilState",
|
||||
"GetTextureBindGeneration",
|
||||
"GetTextureContextId",
|
||||
"GetTextureObject",
|
||||
"GetTextureUnitObject",
|
||||
"GetTransformFeedbackCapturedVertices",
|
||||
"GetTransformFeedbackGeneration",
|
||||
"GetTransformFeedbackPausedPrimitiveCounter",
|
||||
"GetTransformFeedbackProgram",
|
||||
"GetViewport",
|
||||
"GetViewportIndexed",
|
||||
"IsCapabilityEnabled",
|
||||
"IsCapabilityEnabledIndexed",
|
||||
"IsTransformFeedbackActive",
|
||||
"IsTransformFeedbackPaused",
|
||||
"InvalidateCompileEnv",
|
||||
"ValidateProgramName",
|
||||
"RecordError",
|
||||
"GetBoundTransformFeedbackLifetimeId",
|
||||
"HasOpenTransformFeedbackSpan",
|
||||
};
|
||||
|
||||
// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison
|
||||
// cannot protect, so every true is argued for in Coverage.def's
|
||||
// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).
|
||||
inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
|
||||
false, // GetActiveTextureUnit
|
||||
false, // GetBlendColor
|
||||
false, // GetBlendEquationIndexed
|
||||
false, // GetBlendFuncIndexed
|
||||
false, // GetBoundTransformFeedbackName
|
||||
false, // GetBoundVertexArray
|
||||
false, // GetBufferBindingSlot
|
||||
false, // GetBufferBindingPoint
|
||||
true, // GetBufferBindingPointCount: keyed by target: a constexpr capacity table, not verb state
|
||||
false, // GetTouchedBufferBindingPointCount
|
||||
false, // GetClampReadColor
|
||||
false, // GetClearColor
|
||||
false, // GetClearDepth
|
||||
false, // GetClearStencil
|
||||
false, // GetColorMaskIndexed
|
||||
false, // GetCullFaceMode
|
||||
false, // GetCurrentVertexAttribute
|
||||
false, // GetDepthFunc
|
||||
false, // GetDepthMask
|
||||
false, // GetDepthRangeIndexed
|
||||
false, // GetFramebufferBindingSlot
|
||||
false, // GetImageTextureBinding
|
||||
false, // GetLineWidth
|
||||
false, // GetLogicOp
|
||||
false, // GetMaxTouchedTextureUnit
|
||||
false, // GetMinSampleShadingValue
|
||||
false, // GetPatchDefaultInnerLevel
|
||||
false, // GetPatchDefaultOuterLevel
|
||||
false, // GetPatchVertices
|
||||
false, // GetPipelineStateVersion
|
||||
false, // GetPixelStoreParameters
|
||||
false, // GetPolygonModeFront
|
||||
false, // GetPolygonOffsetFactor
|
||||
false, // GetPolygonOffsetUnits
|
||||
false, // GetPrimitiveRestartIndex
|
||||
false, // GetProgramForDispatch
|
||||
false, // GetProgramForDraw
|
||||
true, // GetProgramObject: keyed by GL name: an object lookup, not verb state
|
||||
false, // GetProvokingVertexMode
|
||||
false, // GetRenderStateParameters
|
||||
false, // GetRenderStateParametersVersion
|
||||
false, // GetSamplingResolutionGeneration
|
||||
false, // GetScissorBox
|
||||
false, // GetStencilState
|
||||
false, // GetTextureBindGeneration
|
||||
false, // GetTextureContextId
|
||||
true, // GetTextureObject: keyed by GL name: an object lookup, not verb state
|
||||
false, // GetTextureUnitObject
|
||||
false, // GetTransformFeedbackCapturedVertices
|
||||
false, // GetTransformFeedbackGeneration
|
||||
false, // GetTransformFeedbackPausedPrimitiveCounter
|
||||
false, // GetTransformFeedbackProgram
|
||||
false, // GetViewport
|
||||
false, // GetViewportIndexed
|
||||
false, // IsCapabilityEnabled
|
||||
false, // IsCapabilityEnabledIndexed
|
||||
false, // IsTransformFeedbackActive
|
||||
false, // IsTransformFeedbackPaused
|
||||
true, // InvalidateCompileEnv: reverse channel: a write into the frontend, not a state read
|
||||
true, // ValidateProgramName: keyed by GL name: a name-table lookup, not verb state
|
||||
true, // RecordError: reverse channel: a write into the frontend, not a state read
|
||||
false, // GetBoundTransformFeedbackLifetimeId
|
||||
true, // HasOpenTransformFeedbackSpan: keyed by lifetime id: an object lookup, not verb state
|
||||
};
|
||||
inline constexpr SizeT kMGPipeInputStickyFieldCount = 7;
|
||||
|
||||
// Which call is expected to have filled a field by the time a verb reads it. Names
|
||||
// come from Coverage.def, so this table and the coverage table cannot disagree.
|
||||
inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = {
|
||||
"SetSamplerViews",
|
||||
"SetDynamicState",
|
||||
"CreateRenderState",
|
||||
"CreateRenderState",
|
||||
"SetStreamOutputTargets",
|
||||
"BindVertexElements",
|
||||
"SetIndirectBuffers",
|
||||
"SetShaderBuffers",
|
||||
"SetShaderBuffers",
|
||||
"SetShaderBuffers",
|
||||
"SetDynamicState",
|
||||
"SetDynamicState",
|
||||
"SetDynamicState",
|
||||
"SetDynamicState",
|
||||
"CreateRenderState",
|
||||
"CreateRenderState",
|
||||
"SetVertexAttribDefaults",
|
||||
"CreateRenderState",
|
||||
"CreateRenderState",
|
||||
"SetDynamicState",
|
||||
"SetFramebufferState",
|
||||
"SetShaderImages",
|
||||
"SetDynamicState",
|
||||
"CreateRenderState",
|
||||
"SetSamplerViews",
|
||||
"CreateRenderState",
|
||||
"SetPatchState",
|
||||
"SetPatchState",
|
||||
"SetPatchState",
|
||||
"BindRenderState",
|
||||
"SetPixelPackState",
|
||||
"CreateRenderState",
|
||||
"SetDynamicState",
|
||||
"SetDynamicState",
|
||||
"DrawVbo",
|
||||
"SetDispatchProgram",
|
||||
"SetDrawProgram",
|
||||
"CreateShaderState",
|
||||
"CreateRenderState",
|
||||
"CreateRenderState",
|
||||
"BindRenderState",
|
||||
"SetSamplerViews",
|
||||
"SetDynamicState",
|
||||
"CreateRenderState",
|
||||
"SetSamplerViews",
|
||||
"SetSamplerViews",
|
||||
"SetSamplerViews",
|
||||
"SetSamplerViews",
|
||||
"DrawVbo",
|
||||
"SetStreamOutputTargets",
|
||||
"EndStreamOutput",
|
||||
"SetStreamOutputTargets",
|
||||
"SetDynamicState",
|
||||
"SetDynamicState",
|
||||
"CreateRenderState",
|
||||
"CreateRenderState",
|
||||
"BeginStreamOutput",
|
||||
"PauseStreamOutput",
|
||||
"kClientResolved", // pseudo-call: not filled by a forward record
|
||||
"kClientResolved", // pseudo-call: not filled by a forward record
|
||||
"kReverseChannel", // pseudo-call: not filled by a forward record
|
||||
"SetStreamOutputTargets",
|
||||
"SetStreamOutputTargets",
|
||||
};
|
||||
|
||||
struct MGPipeFilledState {
|
||||
Uint64 CurrentVerbSerial;
|
||||
Uint64 FilledGen[kMGPipeInputFieldCount];
|
||||
};
|
||||
|
||||
[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"}",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], verb);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the
|
||||
// serial is 0 as well, and a read in that window is the poison's "<Field>@<none>" case
|
||||
// (P1 brief D6), never a fresh read of default-constructed storage.
|
||||
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
|
||||
const SizeT index = static_cast<SizeT>(field);
|
||||
const Uint64 gen = state.FilledGen[index];
|
||||
if (gen == 0) return false;
|
||||
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeSpanTable.inc
|
||||
// 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
|
||||
|
||||
// G7: the render-state pipeline subset, by member name.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from the field list in scripts/gen_pipe.py - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
|
||||
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
|
||||
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
|
||||
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
|
||||
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
|
||||
// derived from a list that was reviewed rather than invented.
|
||||
//
|
||||
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
|
||||
// table freezes:
|
||||
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
|
||||
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
|
||||
// backend read points are constant false today. Pipeline state or dead capability?
|
||||
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
|
||||
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
|
||||
// are handled elsewhere in the payload path rather than in the memo word.
|
||||
//
|
||||
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
|
||||
// glViewport must not mint a new CSO.
|
||||
|
||||
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
|
||||
"CullFaceEnabled",
|
||||
"DepthTestEnabled",
|
||||
"PolygonOffsetFillEnabled",
|
||||
"RasterizerDiscardEnabled",
|
||||
"ColorLogicOpEnabled",
|
||||
"StencilTestEnabled",
|
||||
"PrimitiveRestartEnabled",
|
||||
"PrimitiveRestartFixedIndexEnabled",
|
||||
"DepthMask",
|
||||
"SampleShadingEnabled",
|
||||
"MultisampleEnabled",
|
||||
"SampleMaskEnabled",
|
||||
"SampleMaskValue",
|
||||
"MinSampleShadingValue",
|
||||
"PatchVertices",
|
||||
"PatchDefaultOuterLevel",
|
||||
"PatchDefaultInnerLevel",
|
||||
"PolygonModeFront",
|
||||
"CullFaceModeSetting",
|
||||
"DepthFunc",
|
||||
"LogicOp",
|
||||
"StencilStates",
|
||||
"BlendStates",
|
||||
"ColorMasks",
|
||||
};
|
||||
inline constexpr SizeT kMGPipePipelineStateMemberCount = 24;
|
||||
static_assert(kMGPipePipelineStateMemberCount ==
|
||||
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
|
||||
|
||||
// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets
|
||||
// in C++ with offsetof rather than guessing them in python.
|
||||
extern const MGPStateChunk kMGPipePipelineChunks[];
|
||||
extern const MGPStateChunk kMGPipeDynamicChunks[];
|
||||
@@ -0,0 +1,108 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeTables.inc
|
||||
// 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
|
||||
|
||||
// G1: the two MGPipe interface tables.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// share group: 11 calls. A null entry means the backend does not implement this
|
||||
// call and the frontend keeps its own path (plan B section 4.1).
|
||||
struct MGPipeScreen {
|
||||
void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply);
|
||||
void (*ResourceCreate)(const MGPResourceDesc* payload);
|
||||
void (*ResourceRespecify)(const MGPResourceDesc* payload);
|
||||
void (*ResourceDestroy)(const MGPHandleOnly* payload);
|
||||
void (*MapPersistent)(const MGPHandleOnly* payload, MGPReplySlot* reply);
|
||||
void (*UnmapPersistent)(const MGPHandleOnly* payload);
|
||||
void (*FenceCreate)(const MGPHandleOnly* payload);
|
||||
void (*FenceStatus)(const MGPHandleOnly* payload, MGPReplySlot* reply);
|
||||
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
|
||||
void (*FenceDestroy)(const MGPHandleOnly* payload);
|
||||
void (*FenceWaitServer)(const MGPFenceWait* payload);
|
||||
};
|
||||
|
||||
// context: 60 calls. A null entry means the backend does not implement this
|
||||
// call and the frontend keeps its own path (plan B section 4.1).
|
||||
struct MGPipeContext {
|
||||
void (*QueryCreate)(const MGPQueryDesc* payload);
|
||||
void (*QueryBegin)(const MGPQueryDesc* payload);
|
||||
void (*QueryEnd)(const MGPQueryDesc* payload);
|
||||
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
|
||||
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
|
||||
void (*QueryDestroy)(const MGPHandleOnly* payload);
|
||||
void (*CreateRenderState)(const MGPRenderStateDesc* payload);
|
||||
void (*BindRenderState)(const MGPBindRenderState* payload);
|
||||
void (*DeleteRenderState)(const MGPHandleOnly* payload);
|
||||
void (*CreateVertexElements)(const MGPVertexElements* payload);
|
||||
void (*BindVertexElements)(const MGPHandleOnly* payload);
|
||||
void (*DeleteVertexElements)(const MGPHandleOnly* payload);
|
||||
void (*CreateSamplerState)(const MGPSamplerDesc* payload);
|
||||
void (*DeleteSamplerState)(const MGPHandleOnly* payload);
|
||||
void (*CreateSamplerView)(const MGPSamplerView* payload);
|
||||
void (*DeleteSamplerView)(const MGPHandleOnly* payload);
|
||||
void (*CreateShaderState)(const MGPProgramDesc* payload);
|
||||
void (*BindShaderState)(const MGPHandleOnly* payload);
|
||||
void (*DeleteShaderState)(const MGPHandleOnly* payload);
|
||||
void (*SetDynamicState)(const MGPDynamicState* payload);
|
||||
void (*SetFramebufferState)(const MGPFramebufferState* payload);
|
||||
void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
|
||||
void (*SetIndirectBuffers)(const MGPIndirectBuffers* payload);
|
||||
void (*SetSamplerViews)(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*BindSamplerStates)(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetGlobalConstants)(const MGPGlobalConstants* payload);
|
||||
void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetPixelPackState)(const MGPPixelPackState* payload);
|
||||
void (*SetPatchState)(const MGPPatchState* payload);
|
||||
void (*SetDrawProgram)(const MGPHandleOnly* payload);
|
||||
void (*SetDispatchProgram)(const MGPHandleOnly* payload);
|
||||
void (*SetResidualValueState)(const MGPResidualValueState* payload);
|
||||
void (*SetTextureParams)(const MGPTextureParams* payload);
|
||||
void (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*BufferSubDataResident)(const MGPSubData* payload);
|
||||
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
|
||||
void (*ResourceFlushRange)(const MGPFlushRange* payload);
|
||||
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
|
||||
void (*ResourceCopyRegion)(const MGPCopyRegion* payload);
|
||||
void (*GenerateMipmap)(const MGPMipPlan* payload);
|
||||
void (*GetTextureImage)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
|
||||
void (*Blit)(const MGPBlit* payload);
|
||||
void (*Clear)(const MGPClear* payload);
|
||||
void (*ReadPixels)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
|
||||
void (*DrawVbo)(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*LaunchGrid)(const MGPGridInfo* payload);
|
||||
void (*MemoryBarrier)(const MGPMemoryBarrier* payload);
|
||||
void (*BeginStreamOutput)(const MGPStreamOutputBegin* payload);
|
||||
void (*EndStreamOutput)(const MGPXfbAccounting* payload);
|
||||
void (*PauseStreamOutput)(const MGPStreamOutputControl* payload);
|
||||
void (*ResumeStreamOutput)(const MGPStreamOutputControl* payload);
|
||||
void (*Flush)(const MGPFlush* payload);
|
||||
void (*Present)(const MGPPresent* payload);
|
||||
void (*SetSwapInterval)(const MGPSwapInterval* payload);
|
||||
void (*QueryTimestamp)(const MGPTimestampRequest* payload, MGPReplySlot* reply);
|
||||
void (*QueryCounter)(const MGPQueryDesc* payload);
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeScreenCallCount = 11;
|
||||
inline constexpr SizeT kMGPipeContextCallCount = 60;
|
||||
inline constexpr SizeT kMGPipeCallCount = 71;
|
||||
|
||||
// A table that is not exactly its call count of function pointers has grown a
|
||||
// member that no generator knows about.
|
||||
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()),
|
||||
"MGPipeScreen is not exactly its catalogue's function pointers");
|
||||
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()),
|
||||
"MGPipeContext is not exactly its catalogue's function pointers");
|
||||
static_assert(kMGPipeScreenCallCount + kMGPipeContextCallCount == kMGPipeCallCount);
|
||||
static_assert(kMGPipeCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT,
|
||||
"the catalogue and its documented count disagree");
|
||||
@@ -0,0 +1,302 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeThunks.inc
|
||||
// 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
|
||||
|
||||
// G2: monolith thunks over the two tables.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// One inline call through the installed table. These are the names MG_Impl call
|
||||
// sites move onto, replacing gBackendFunctionsTable.GL.* one at a time. An
|
||||
// unimplemented (null) entry is the caller's business to check, exactly as it is
|
||||
// with the table this replaces.
|
||||
|
||||
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.GetCaps(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceCreate(const MGPResourceDesc* payload) {
|
||||
gMGPipeScreen.ResourceCreate(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceRespecify(const MGPResourceDesc* payload) {
|
||||
gMGPipeScreen.ResourceRespecify(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceDestroy(const MGPHandleOnly* payload) {
|
||||
gMGPipeScreen.ResourceDestroy(payload);
|
||||
}
|
||||
|
||||
inline void MGP_MapPersistent(const MGPHandleOnly* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.MapPersistent(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_UnmapPersistent(const MGPHandleOnly* payload) {
|
||||
gMGPipeScreen.UnmapPersistent(payload);
|
||||
}
|
||||
|
||||
inline void MGP_FenceCreate(const MGPHandleOnly* payload) {
|
||||
gMGPipeScreen.FenceCreate(payload);
|
||||
}
|
||||
|
||||
inline void MGP_FenceStatus(const MGPHandleOnly* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.FenceStatus(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_FenceWait(const MGPFenceWait* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.FenceWait(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_FenceDestroy(const MGPHandleOnly* payload) {
|
||||
gMGPipeScreen.FenceDestroy(payload);
|
||||
}
|
||||
|
||||
inline void MGP_QueryCreate(const MGPQueryDesc* payload) {
|
||||
gMGPipeContext.QueryCreate(payload);
|
||||
}
|
||||
|
||||
inline void MGP_QueryBegin(const MGPQueryDesc* payload) {
|
||||
gMGPipeContext.QueryBegin(payload);
|
||||
}
|
||||
|
||||
inline void MGP_QueryEnd(const MGPQueryDesc* payload) {
|
||||
gMGPipeContext.QueryEnd(payload);
|
||||
}
|
||||
|
||||
inline void MGP_QueryAvailable(const MGPHandleOnly* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.QueryAvailable(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_QueryResult(const MGPQueryResultRequest* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.QueryResult(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.QueryDestroy(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) {
|
||||
gMGPipeContext.CreateRenderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
|
||||
gMGPipeContext.BindRenderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteRenderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) {
|
||||
gMGPipeContext.CreateVertexElements(payload);
|
||||
}
|
||||
|
||||
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.BindVertexElements(payload);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteVertexElements(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) {
|
||||
gMGPipeContext.CreateSamplerState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteSamplerState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateSamplerView(const MGPSamplerView* payload) {
|
||||
gMGPipeContext.CreateSamplerView(payload);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteSamplerView(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) {
|
||||
gMGPipeContext.CreateShaderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.BindShaderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteShaderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetDynamicState(const MGPDynamicState* payload) {
|
||||
gMGPipeContext.SetDynamicState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
|
||||
gMGPipeContext.SetFramebufferState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetVertexBuffers(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetVertexBuffers(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetIndexBuffer(const MGPIndexBuffer* payload) {
|
||||
gMGPipeContext.SetIndexBuffer(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetIndirectBuffers(const MGPIndirectBuffers* payload) {
|
||||
gMGPipeContext.SetIndirectBuffers(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetSamplerViews(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetSamplerViews(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindSamplerStates(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.BindSamplerStates(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetShaderImages(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetShaderImages(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetShaderBuffers(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetShaderBuffers(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) {
|
||||
gMGPipeContext.SetGlobalConstants(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.SetVertexAttribDefaults(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetPixelPackState(const MGPPixelPackState* payload) {
|
||||
gMGPipeContext.SetPixelPackState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetPatchState(const MGPPatchState* payload) {
|
||||
gMGPipeContext.SetPatchState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetDrawProgram(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.SetDrawProgram(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetDispatchProgram(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.SetDispatchProgram(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) {
|
||||
gMGPipeContext.SetResidualValueState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetTextureParams(const MGPTextureParams* payload) {
|
||||
gMGPipeContext.SetTextureParams(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_BufferSubDataResident(const MGPSubData* payload) {
|
||||
gMGPipeContext.BufferSubDataResident(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
|
||||
gMGPipeContext.ResourceSubDataComplete(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceFlushRange(const MGPFlushRange* payload) {
|
||||
gMGPipeContext.ResourceFlushRange(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ResourceReadback(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceCopyRegion(const MGPCopyRegion* payload) {
|
||||
gMGPipeContext.ResourceCopyRegion(payload);
|
||||
}
|
||||
|
||||
inline void MGP_GenerateMipmap(const MGPMipPlan* payload) {
|
||||
gMGPipeContext.GenerateMipmap(payload);
|
||||
}
|
||||
|
||||
inline void MGP_GetTextureImage(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.GetTextureImage(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_Blit(const MGPBlit* payload) {
|
||||
gMGPipeContext.Blit(payload);
|
||||
}
|
||||
|
||||
inline void MGP_Clear(const MGPClear* payload) {
|
||||
gMGPipeContext.Clear(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ReadPixels(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ReadPixels(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_DrawVbo(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount) {
|
||||
gMGPipeContext.DrawVbo(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_LaunchGrid(const MGPGridInfo* payload) {
|
||||
gMGPipeContext.LaunchGrid(payload);
|
||||
}
|
||||
|
||||
inline void MGP_MemoryBarrier(const MGPMemoryBarrier* payload) {
|
||||
gMGPipeContext.MemoryBarrier(payload);
|
||||
}
|
||||
|
||||
inline void MGP_BeginStreamOutput(const MGPStreamOutputBegin* payload) {
|
||||
gMGPipeContext.BeginStreamOutput(payload);
|
||||
}
|
||||
|
||||
inline void MGP_EndStreamOutput(const MGPXfbAccounting* payload) {
|
||||
gMGPipeContext.EndStreamOutput(payload);
|
||||
}
|
||||
|
||||
inline void MGP_PauseStreamOutput(const MGPStreamOutputControl* payload) {
|
||||
gMGPipeContext.PauseStreamOutput(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ResumeStreamOutput(const MGPStreamOutputControl* payload) {
|
||||
gMGPipeContext.ResumeStreamOutput(payload);
|
||||
}
|
||||
|
||||
inline void MGP_Flush(const MGPFlush* payload) {
|
||||
gMGPipeContext.Flush(payload);
|
||||
}
|
||||
|
||||
inline void MGP_Present(const MGPPresent* payload) {
|
||||
gMGPipeContext.Present(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetSwapInterval(const MGPSwapInterval* payload) {
|
||||
gMGPipeContext.SetSwapInterval(payload);
|
||||
}
|
||||
|
||||
inline void MGP_QueryTimestamp(const MGPTimestampRequest* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.QueryTimestamp(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_QueryCounter(const MGPQueryDesc* payload) {
|
||||
gMGPipeContext.QueryCounter(payload);
|
||||
}
|
||||
|
||||
inline void MGP_FenceWaitServer(const MGPFenceWait* payload) {
|
||||
gMGPipeScreen.FenceWaitServer(payload);
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeVerify.inc
|
||||
// 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
|
||||
|
||||
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from PipeFields.def - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// Field by field, never memcmp over a whole payload: RenderStateParameters is documented
|
||||
// in DirectGLES.cpp to false-DIFFER on padding under memcmp (harmlessly there, fatally
|
||||
// here - a comparator with false positives is a comparator nobody reads). Each function
|
||||
// reports the FIRST differing field by name, which with the draw serial is what the verify
|
||||
// harness prints.
|
||||
//
|
||||
// Floating-point fields are compared by BITS, so a NaN patch level - which
|
||||
// glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals
|
||||
// itself instead of tripping every draw.
|
||||
|
||||
#include "../PipeFields.def"
|
||||
|
||||
template <class T>
|
||||
struct MGPipeHasFieldVerifier : std::false_type {};
|
||||
|
||||
// A vector type (FloatVec4, IntVec4, BoolVec4...) is detected through its VecBase and
|
||||
// compared BITWISE over its data: VecBase::operator== is IEEE ==, under which a NaN patch
|
||||
// level would differ from itself. The probe rather than an overload because a
|
||||
// derived-to-base conversion loses overload resolution to the exact-match generic template.
|
||||
template <class Derived, class T, SizeT N>
|
||||
std::true_type MGPipeVecBaseProbe(const VecBase<Derived, T, N>*);
|
||||
std::false_type MGPipeVecBaseProbe(const void*);
|
||||
template <class T>
|
||||
inline constexpr Bool kMGPipeIsVecBase = decltype(MGPipeVecBaseProbe(static_cast<const T*>(nullptr)))::value;
|
||||
|
||||
template <class T>
|
||||
inline Bool MGPipeFieldEqual(const T& a, const T& b);
|
||||
template <class T, SizeT N>
|
||||
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b);
|
||||
template <class T, SizeT N>
|
||||
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]);
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
|
||||
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField);
|
||||
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPRange> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBox> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPReplySlot> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPStateChunk> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPHandleOnly> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPCaps> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPResourceDesc> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPFenceWait> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPQueryDesc> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPQueryResultRequest> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPTimestampRequest> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPRenderStateDesc> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBindRenderState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPDynamicState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPVertexElements> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSamplerDesc> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSamplerView> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPTextureParams> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPProgramDesc> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSurface> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPFramebufferState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPVertexBuffer> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPVertexBuffers> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPIndexBuffer> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPIndirectBuffers> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBoundView> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSamplerViews> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSamplerStates> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPImageView> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPShaderImages> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBufferRange> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPShaderBuffers> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPStreamOutputTargets> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPGlobalConstants> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPAttribValue> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPVertexAttribDefaults> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPPixelPackState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPPatchState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<ResidualValueBlock> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPResidualValueState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSubRegion> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSubData> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSubDataComplete> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPFlushRange> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPReadback> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPCopyRegion> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPBlit> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPClear> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPMipPlan> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPReadbackInfo> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPDrawInfo> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPDrawRange> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPDrawIndirect> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPGridInfo> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPMemoryBarrier> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPStreamOutputBegin> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPXfbAccounting> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPStreamOutputControl> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPFlush> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPPresent> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSwapInterval> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPSurfaceInfo> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<DynamicBackendParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGHostSpan> : std::true_type {};
|
||||
|
||||
template <class T>
|
||||
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
||||
if constexpr (MGPipeHasFieldVerifier<T>::value) {
|
||||
const char* unusedField = nullptr;
|
||||
return MGPipeVerify(a, b, &unusedField);
|
||||
} else if constexpr (kMGPipeIsVecBase<T>) {
|
||||
return std::memcmp(a.data.data(), b.data.data(), sizeof(a.data)) == 0;
|
||||
} else if constexpr (std::is_floating_point_v<T>) {
|
||||
return std::memcmp(&a, &b, sizeof(T)) == 0;
|
||||
} else if constexpr (std::is_scalar_v<T> || std::is_enum_v<T>) {
|
||||
return a == b;
|
||||
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
|
||||
return a == b;
|
||||
} else {
|
||||
// NO MEMCMP FALLBACK. Every value struct has a field list in PipeFields.def since P1
|
||||
// (and gen_pipe.py asserts each list covers its struct's members); a type reaching
|
||||
// this branch is one nobody gave a field list, and a memcmp would false-differ on
|
||||
// its padding. A compile error is the honest answer.
|
||||
static_assert(sizeof(T) == 0, "no field list in PipeFields.def for this type");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <class T, SizeT N>
|
||||
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b) {
|
||||
for (SizeT i = 0; i < N; ++i) {
|
||||
if (!MGPipeFieldEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class T, SizeT N>
|
||||
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
|
||||
for (SizeT i = 0; i < N; ++i) {
|
||||
if (!MGPipeFieldEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#define MGP_VERIFY_FIELD(FieldName) \
|
||||
if (!MGPipeFieldEqual(a.FieldName, b.FieldName)) { \
|
||||
if (outField != nullptr) *outField = #FieldName; \
|
||||
return false; \
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBlobRef(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField) {
|
||||
MGP_FIELDS_MGPRange(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBox(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField) {
|
||||
MGP_FIELDS_MGPReplySlot(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField) {
|
||||
MGP_FIELDS_MGPStateChunk(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField) {
|
||||
MGP_FIELDS_MGPHandleOnly(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField) {
|
||||
MGP_FIELDS_MGPCaps(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField) {
|
||||
MGP_FIELDS_MGPResourceDesc(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField) {
|
||||
MGP_FIELDS_MGPFenceWait(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField) {
|
||||
MGP_FIELDS_MGPQueryDesc(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField) {
|
||||
MGP_FIELDS_MGPQueryResultRequest(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField) {
|
||||
MGP_FIELDS_MGPTimestampRequest(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField) {
|
||||
MGP_FIELDS_MGPRenderStateDesc(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBindRenderState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPDynamicState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField) {
|
||||
MGP_FIELDS_MGPVertexElements(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSamplerDesc(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSamplerView(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField) {
|
||||
MGP_FIELDS_MGPTextureParams(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField) {
|
||||
MGP_FIELDS_MGPProgramDesc(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSurface(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPFramebufferState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField) {
|
||||
MGP_FIELDS_MGPVertexBuffer(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField) {
|
||||
MGP_FIELDS_MGPVertexBuffers(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField) {
|
||||
MGP_FIELDS_MGPIndexBuffer(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField) {
|
||||
MGP_FIELDS_MGPIndirectBuffers(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBoundView(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSamplerViews(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSamplerStates(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField) {
|
||||
MGP_FIELDS_MGPImageView(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField) {
|
||||
MGP_FIELDS_MGPShaderImages(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBufferRange(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField) {
|
||||
MGP_FIELDS_MGPShaderBuffers(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField) {
|
||||
MGP_FIELDS_MGPStreamOutputTargets(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField) {
|
||||
MGP_FIELDS_MGPGlobalConstants(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField) {
|
||||
MGP_FIELDS_MGPAttribValue(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField) {
|
||||
MGP_FIELDS_MGPVertexAttribDefaults(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPPixelPackState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPPatchState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField) {
|
||||
MGP_FIELDS_ResidualValueBlock(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField) {
|
||||
MGP_FIELDS_MGPResidualValueState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSubRegion(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSubData(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSubDataComplete(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField) {
|
||||
MGP_FIELDS_MGPFlushRange(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField) {
|
||||
MGP_FIELDS_MGPReadback(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField) {
|
||||
MGP_FIELDS_MGPCopyRegion(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField) {
|
||||
MGP_FIELDS_MGPBlit(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField) {
|
||||
MGP_FIELDS_MGPClear(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField) {
|
||||
MGP_FIELDS_MGPMipPlan(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField) {
|
||||
MGP_FIELDS_MGPReadbackInfo(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField) {
|
||||
MGP_FIELDS_MGPDrawInfo(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField) {
|
||||
MGP_FIELDS_MGPDrawRange(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField) {
|
||||
MGP_FIELDS_MGPDrawIndirect(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField) {
|
||||
MGP_FIELDS_MGPGridInfo(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField) {
|
||||
MGP_FIELDS_MGPMemoryBarrier(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField) {
|
||||
MGP_FIELDS_MGPStreamOutputBegin(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField) {
|
||||
MGP_FIELDS_MGPXfbAccounting(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField) {
|
||||
MGP_FIELDS_MGPStreamOutputControl(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField) {
|
||||
MGP_FIELDS_MGPFlush(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField) {
|
||||
MGP_FIELDS_MGPPresent(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSwapInterval(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField) {
|
||||
MGP_FIELDS_MGPSurfaceInfo(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
|
||||
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField) {
|
||||
MGP_FIELDS_PixelStoreParameters(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
|
||||
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField) {
|
||||
MGP_FIELDS_StencilFaceState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField) {
|
||||
MGP_FIELDS_DynamicBackendParameters(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField) {
|
||||
MGP_FIELDS_MGHostSpan(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef MGP_VERIFY_FIELD
|
||||
|
||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
|
||||
@@ -0,0 +1,931 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/generated/PipeWire.inc
|
||||
// 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
|
||||
|
||||
// G3: wire records, size assertions and the applier's bounds gate.
|
||||
//
|
||||
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
|
||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||
|
||||
// Every record is a fixed header plus its payload, padded to the stream's 8-byte
|
||||
// granularity. The size assertion is stated as a COMPOSITION so it fires on any padding
|
||||
// the compiler inserts between the header and the payload while staying honest about the
|
||||
// tail padding the alignment requires.
|
||||
//
|
||||
// The applier's precondition is checked BEFORE dispatch, on every record, in every build:
|
||||
// a record that is shorter than its own type, longer than what is left in the buffer, or
|
||||
// not a multiple of 8 is protocol corruption and is fatal. There is no recovery path -
|
||||
// silently applying a truncated record is how a corrupt stream becomes a wrong picture.
|
||||
//
|
||||
// OVERSIZED PAYLOADS ARE CHUNKED, NEVER EMITTED WHOLE (plan section 8.2: G3 has to define
|
||||
// the path for a record larger than the segment). The bound is the ring's,
|
||||
// RingProducer::MaxRecordBytes() == Capacity()/2, and it is exact rather than
|
||||
// conservative: a record has to be placeable at every head offset of an empty ring, the
|
||||
// wrap pad in front of it costs up to total-8 bytes, and only a record of at most half the
|
||||
// ring survives that at every offset. An emitter holding more than Capacity()/2 bytes of
|
||||
// record (a large resource_subdata, a create_shader_state archive) splits it into several
|
||||
// records of at most that size; the transport refuses a bigger one outright - nullptr plus
|
||||
// an MGLOG_E - rather than let the producer wait on free bytes that can never suffice.
|
||||
|
||||
struct MGPWireRecHeader {
|
||||
Uint16 Op; // MGPWireOp
|
||||
Uint16 Flags; // MGPipeCallFlags of the call, for asserts and tracing
|
||||
Uint32 Size; // bytes of this record including the header and the variable tail
|
||||
};
|
||||
static_assert(sizeof(MGPWireRecHeader) == 8, "the wire header is 8 bytes");
|
||||
static_assert(std::is_trivially_copyable_v<MGPWireRecHeader>);
|
||||
|
||||
// The opcode is the call's position in PipeCalls.def. Reordering that file is a protocol
|
||||
// break; appending to it is not.
|
||||
enum class MGPWireOp : Uint16 {
|
||||
kInvalid = 0,
|
||||
GetCaps = 1,
|
||||
ResourceCreate = 2,
|
||||
ResourceRespecify = 3,
|
||||
ResourceDestroy = 4,
|
||||
MapPersistent = 5,
|
||||
UnmapPersistent = 6,
|
||||
FenceCreate = 7,
|
||||
FenceStatus = 8,
|
||||
FenceWait = 9,
|
||||
FenceDestroy = 10,
|
||||
QueryCreate = 11,
|
||||
QueryBegin = 12,
|
||||
QueryEnd = 13,
|
||||
QueryAvailable = 14,
|
||||
QueryResult = 15,
|
||||
QueryDestroy = 16,
|
||||
CreateRenderState = 17,
|
||||
BindRenderState = 18,
|
||||
DeleteRenderState = 19,
|
||||
CreateVertexElements = 20,
|
||||
BindVertexElements = 21,
|
||||
DeleteVertexElements = 22,
|
||||
CreateSamplerState = 23,
|
||||
DeleteSamplerState = 24,
|
||||
CreateSamplerView = 25,
|
||||
DeleteSamplerView = 26,
|
||||
CreateShaderState = 27,
|
||||
BindShaderState = 28,
|
||||
DeleteShaderState = 29,
|
||||
SetDynamicState = 30,
|
||||
SetFramebufferState = 31,
|
||||
SetVertexBuffers = 32,
|
||||
SetIndexBuffer = 33,
|
||||
SetIndirectBuffers = 34,
|
||||
SetSamplerViews = 35,
|
||||
BindSamplerStates = 36,
|
||||
SetShaderImages = 37,
|
||||
SetShaderBuffers = 38,
|
||||
SetStreamOutputTargets = 39,
|
||||
SetGlobalConstants = 40,
|
||||
SetVertexAttribDefaults = 41,
|
||||
SetPixelPackState = 42,
|
||||
SetPatchState = 43,
|
||||
SetDrawProgram = 44,
|
||||
SetDispatchProgram = 45,
|
||||
SetResidualValueState = 46,
|
||||
SetTextureParams = 47,
|
||||
ResourceSubData = 48,
|
||||
BufferSubDataResident = 49,
|
||||
ResourceSubDataComplete = 50,
|
||||
ResourceFlushRange = 51,
|
||||
ResourceReadback = 52,
|
||||
ResourceCopyRegion = 53,
|
||||
GenerateMipmap = 54,
|
||||
GetTextureImage = 55,
|
||||
Blit = 56,
|
||||
Clear = 57,
|
||||
ReadPixels = 58,
|
||||
DrawVbo = 59,
|
||||
LaunchGrid = 60,
|
||||
MemoryBarrier = 61,
|
||||
BeginStreamOutput = 62,
|
||||
EndStreamOutput = 63,
|
||||
PauseStreamOutput = 64,
|
||||
ResumeStreamOutput = 65,
|
||||
Flush = 66,
|
||||
Present = 67,
|
||||
SetSwapInterval = 68,
|
||||
QueryTimestamp = 69,
|
||||
QueryCounter = 70,
|
||||
FenceWaitServer = 71,
|
||||
kOpCount = 72,
|
||||
};
|
||||
|
||||
struct alignas(8) MGPWireRec_GetCaps {
|
||||
MGPWireRecHeader Header;
|
||||
MGPCaps Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_GetCaps) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPCaps) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_GetCaps gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceCreate {
|
||||
MGPWireRecHeader Header;
|
||||
MGPResourceDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceCreate) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceCreate gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceRespecify {
|
||||
MGPWireRecHeader Header;
|
||||
MGPResourceDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceRespecify) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceRespecify gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceDestroy {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceDestroy) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceDestroy gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_MapPersistent {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_MapPersistent) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_MapPersistent gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_UnmapPersistent {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_UnmapPersistent) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_UnmapPersistent gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_FenceCreate {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_FenceCreate) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_FenceCreate gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_FenceStatus {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_FenceStatus) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_FenceStatus gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_FenceWait {
|
||||
MGPWireRecHeader Header;
|
||||
MGPFenceWait Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_FenceWait) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_FenceWait gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_FenceDestroy {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_FenceDestroy) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_FenceDestroy gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryCreate {
|
||||
MGPWireRecHeader Header;
|
||||
MGPQueryDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryCreate) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryCreate gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryBegin {
|
||||
MGPWireRecHeader Header;
|
||||
MGPQueryDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryBegin) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryBegin gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryEnd {
|
||||
MGPWireRecHeader Header;
|
||||
MGPQueryDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryEnd) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryEnd gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryAvailable {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryAvailable) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryAvailable gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryResult {
|
||||
MGPWireRecHeader Header;
|
||||
MGPQueryResultRequest Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryResult) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryResultRequest) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryResult gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryDestroy {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryDestroy) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryDestroy gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_CreateRenderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPRenderStateDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_CreateRenderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPRenderStateDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_CreateRenderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BindRenderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPBindRenderState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BindRenderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPBindRenderState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BindRenderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DeleteRenderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DeleteRenderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DeleteRenderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_CreateVertexElements {
|
||||
MGPWireRecHeader Header;
|
||||
MGPVertexElements Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_CreateVertexElements) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexElements) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_CreateVertexElements gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BindVertexElements {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BindVertexElements) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BindVertexElements gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DeleteVertexElements {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DeleteVertexElements) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DeleteVertexElements gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_CreateSamplerState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSamplerDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_CreateSamplerState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_CreateSamplerState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DeleteSamplerState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DeleteSamplerState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DeleteSamplerState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_CreateSamplerView {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSamplerView Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_CreateSamplerView) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerView) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_CreateSamplerView gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DeleteSamplerView {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DeleteSamplerView) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DeleteSamplerView gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_CreateShaderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPProgramDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_CreateShaderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPProgramDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_CreateShaderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BindShaderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BindShaderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BindShaderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DeleteShaderState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DeleteShaderState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DeleteShaderState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetDynamicState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPDynamicState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetDynamicState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPDynamicState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetDynamicState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetFramebufferState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPFramebufferState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetFramebufferState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPFramebufferState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetFramebufferState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetVertexBuffers {
|
||||
MGPWireRecHeader Header;
|
||||
MGPVertexBuffers Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetVertexBuffers) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexBuffers) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetVertexBuffers gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetIndexBuffer {
|
||||
MGPWireRecHeader Header;
|
||||
MGPIndexBuffer Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetIndexBuffer) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPIndexBuffer) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetIndexBuffer gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetIndirectBuffers {
|
||||
MGPWireRecHeader Header;
|
||||
MGPIndirectBuffers Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetIndirectBuffers) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPIndirectBuffers) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetIndirectBuffers gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetSamplerViews {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSamplerViews Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetSamplerViews) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerViews) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetSamplerViews gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BindSamplerStates {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSamplerStates Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BindSamplerStates) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerStates) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BindSamplerStates gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetShaderImages {
|
||||
MGPWireRecHeader Header;
|
||||
MGPShaderImages Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetShaderImages) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderImages) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetShaderImages gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetShaderBuffers {
|
||||
MGPWireRecHeader Header;
|
||||
MGPShaderBuffers Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetShaderBuffers) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderBuffers) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetShaderBuffers gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetStreamOutputTargets {
|
||||
MGPWireRecHeader Header;
|
||||
MGPStreamOutputTargets Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetStreamOutputTargets) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputTargets) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetStreamOutputTargets gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetGlobalConstants {
|
||||
MGPWireRecHeader Header;
|
||||
MGPGlobalConstants Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetGlobalConstants) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPGlobalConstants) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetGlobalConstants gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetVertexAttribDefaults {
|
||||
MGPWireRecHeader Header;
|
||||
MGPVertexAttribDefaults Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetVertexAttribDefaults) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexAttribDefaults) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetVertexAttribDefaults gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetPixelPackState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPPixelPackState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetPixelPackState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPPixelPackState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetPixelPackState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetPatchState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPPatchState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetPatchState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPPatchState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetPatchState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetDrawProgram {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetDrawProgram) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetDrawProgram gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetDispatchProgram {
|
||||
MGPWireRecHeader Header;
|
||||
MGPHandleOnly Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetDispatchProgram) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetDispatchProgram gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetResidualValueState {
|
||||
MGPWireRecHeader Header;
|
||||
MGPResidualValueState Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetResidualValueState) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPResidualValueState) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetResidualValueState gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetTextureParams {
|
||||
MGPWireRecHeader Header;
|
||||
MGPTextureParams Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetTextureParams) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPTextureParams) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetTextureParams gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceSubData {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSubData Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceSubData) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceSubData gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BufferSubDataResident {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSubData Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BufferSubDataResident) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BufferSubDataResident gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceSubDataComplete {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSubDataComplete Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceSubDataComplete) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSubDataComplete) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceSubDataComplete gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceFlushRange {
|
||||
MGPWireRecHeader Header;
|
||||
MGPFlushRange Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceFlushRange) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPFlushRange) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceFlushRange gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceReadback {
|
||||
MGPWireRecHeader Header;
|
||||
MGPReadback Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceReadback) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPReadback) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceReadback gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResourceCopyRegion {
|
||||
MGPWireRecHeader Header;
|
||||
MGPCopyRegion Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResourceCopyRegion) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyRegion) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResourceCopyRegion gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_GenerateMipmap {
|
||||
MGPWireRecHeader Header;
|
||||
MGPMipPlan Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_GenerateMipmap) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPMipPlan) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_GenerateMipmap gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_GetTextureImage {
|
||||
MGPWireRecHeader Header;
|
||||
MGPReadbackInfo Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_GetTextureImage) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_GetTextureImage gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_Blit {
|
||||
MGPWireRecHeader Header;
|
||||
MGPBlit Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_Blit) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPBlit) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_Blit gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_Clear {
|
||||
MGPWireRecHeader Header;
|
||||
MGPClear Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_Clear) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPClear) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_Clear gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ReadPixels {
|
||||
MGPWireRecHeader Header;
|
||||
MGPReadbackInfo Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ReadPixels) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ReadPixels gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_DrawVbo {
|
||||
MGPWireRecHeader Header;
|
||||
MGPDrawInfo Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_DrawVbo) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPDrawInfo) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_DrawVbo gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_LaunchGrid {
|
||||
MGPWireRecHeader Header;
|
||||
MGPGridInfo Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_LaunchGrid) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPGridInfo) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_LaunchGrid gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_MemoryBarrier {
|
||||
MGPWireRecHeader Header;
|
||||
MGPMemoryBarrier Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_MemoryBarrier) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPMemoryBarrier) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_MemoryBarrier gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_BeginStreamOutput {
|
||||
MGPWireRecHeader Header;
|
||||
MGPStreamOutputBegin Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_BeginStreamOutput) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputBegin) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_BeginStreamOutput gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_EndStreamOutput {
|
||||
MGPWireRecHeader Header;
|
||||
MGPXfbAccounting Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_EndStreamOutput) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPXfbAccounting) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_EndStreamOutput gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_PauseStreamOutput {
|
||||
MGPWireRecHeader Header;
|
||||
MGPStreamOutputControl Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_PauseStreamOutput) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_PauseStreamOutput gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_ResumeStreamOutput {
|
||||
MGPWireRecHeader Header;
|
||||
MGPStreamOutputControl Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_ResumeStreamOutput) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_ResumeStreamOutput gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_Flush {
|
||||
MGPWireRecHeader Header;
|
||||
MGPFlush Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_Flush) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPFlush) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_Flush gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_Present {
|
||||
MGPWireRecHeader Header;
|
||||
MGPPresent Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_Present) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPPresent) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_Present gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_SetSwapInterval {
|
||||
MGPWireRecHeader Header;
|
||||
MGPSwapInterval Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_SetSwapInterval) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPSwapInterval) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_SetSwapInterval gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryTimestamp {
|
||||
MGPWireRecHeader Header;
|
||||
MGPTimestampRequest Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryTimestamp) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPTimestampRequest) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryTimestamp gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_QueryCounter {
|
||||
MGPWireRecHeader Header;
|
||||
MGPQueryDesc Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_QueryCounter) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_QueryCounter gained padding; the wire format moved");
|
||||
|
||||
struct alignas(8) MGPWireRec_FenceWaitServer {
|
||||
MGPWireRecHeader Header;
|
||||
MGPFenceWait Payload;
|
||||
};
|
||||
static_assert(sizeof(MGPWireRec_FenceWaitServer) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
|
||||
"MGPWireRec_FenceWaitServer gained padding; the wire format moved");
|
||||
|
||||
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
|
||||
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
||||
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
#define MGP_WIRE_CHECK_BOUNDS(RecType, CallName) \
|
||||
do { \
|
||||
if (!(size >= sizeof(RecType) && size <= remaining && (size % 8) == 0)) { \
|
||||
MGPipeWireProtocolFatal(CallName, size, remaining); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Returns whether the record was applied. P0 is a SKELETON: every case validates its
|
||||
// bounds and then reports "not applied", because no applier exists until P5 wires
|
||||
// MG_Remote/Server/PipeApplier.cpp to the real backend tables. The switch and the opcode
|
||||
// enum come from the same list, so a call added to the catalogue cannot be forgotten here;
|
||||
// the default arm is for the opcode that never came from this catalogue at all - a byte
|
||||
// off a corrupt stream - and it is fatal for the same reason the bounds check is.
|
||||
inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, Uint64 remaining) {
|
||||
(void)record;
|
||||
switch (op) {
|
||||
case MGPWireOp::GetCaps:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetCaps, "GetCaps");
|
||||
return false;
|
||||
case MGPWireOp::ResourceCreate:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCreate, "ResourceCreate");
|
||||
return false;
|
||||
case MGPWireOp::ResourceRespecify:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceRespecify, "ResourceRespecify");
|
||||
return false;
|
||||
case MGPWireOp::ResourceDestroy:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceDestroy, "ResourceDestroy");
|
||||
return false;
|
||||
case MGPWireOp::MapPersistent:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MapPersistent, "MapPersistent");
|
||||
return false;
|
||||
case MGPWireOp::UnmapPersistent:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_UnmapPersistent, "UnmapPersistent");
|
||||
return false;
|
||||
case MGPWireOp::FenceCreate:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceCreate, "FenceCreate");
|
||||
return false;
|
||||
case MGPWireOp::FenceStatus:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceStatus, "FenceStatus");
|
||||
return false;
|
||||
case MGPWireOp::FenceWait:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWait, "FenceWait");
|
||||
return false;
|
||||
case MGPWireOp::FenceDestroy:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceDestroy, "FenceDestroy");
|
||||
return false;
|
||||
case MGPWireOp::QueryCreate:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCreate, "QueryCreate");
|
||||
return false;
|
||||
case MGPWireOp::QueryBegin:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryBegin, "QueryBegin");
|
||||
return false;
|
||||
case MGPWireOp::QueryEnd:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryEnd, "QueryEnd");
|
||||
return false;
|
||||
case MGPWireOp::QueryAvailable:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryAvailable, "QueryAvailable");
|
||||
return false;
|
||||
case MGPWireOp::QueryResult:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryResult, "QueryResult");
|
||||
return false;
|
||||
case MGPWireOp::QueryDestroy:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryDestroy, "QueryDestroy");
|
||||
return false;
|
||||
case MGPWireOp::CreateRenderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateRenderState, "CreateRenderState");
|
||||
return false;
|
||||
case MGPWireOp::BindRenderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindRenderState, "BindRenderState");
|
||||
return false;
|
||||
case MGPWireOp::DeleteRenderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteRenderState, "DeleteRenderState");
|
||||
return false;
|
||||
case MGPWireOp::CreateVertexElements:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateVertexElements, "CreateVertexElements");
|
||||
return false;
|
||||
case MGPWireOp::BindVertexElements:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindVertexElements, "BindVertexElements");
|
||||
return false;
|
||||
case MGPWireOp::DeleteVertexElements:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteVertexElements, "DeleteVertexElements");
|
||||
return false;
|
||||
case MGPWireOp::CreateSamplerState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerState, "CreateSamplerState");
|
||||
return false;
|
||||
case MGPWireOp::DeleteSamplerState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerState, "DeleteSamplerState");
|
||||
return false;
|
||||
case MGPWireOp::CreateSamplerView:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerView, "CreateSamplerView");
|
||||
return false;
|
||||
case MGPWireOp::DeleteSamplerView:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerView, "DeleteSamplerView");
|
||||
return false;
|
||||
case MGPWireOp::CreateShaderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateShaderState, "CreateShaderState");
|
||||
return false;
|
||||
case MGPWireOp::BindShaderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindShaderState, "BindShaderState");
|
||||
return false;
|
||||
case MGPWireOp::DeleteShaderState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteShaderState, "DeleteShaderState");
|
||||
return false;
|
||||
case MGPWireOp::SetDynamicState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDynamicState, "SetDynamicState");
|
||||
return false;
|
||||
case MGPWireOp::SetFramebufferState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetFramebufferState, "SetFramebufferState");
|
||||
return false;
|
||||
case MGPWireOp::SetVertexBuffers:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexBuffers, "SetVertexBuffers");
|
||||
return false;
|
||||
case MGPWireOp::SetIndexBuffer:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndexBuffer, "SetIndexBuffer");
|
||||
return false;
|
||||
case MGPWireOp::SetIndirectBuffers:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndirectBuffers, "SetIndirectBuffers");
|
||||
return false;
|
||||
case MGPWireOp::SetSamplerViews:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSamplerViews, "SetSamplerViews");
|
||||
return false;
|
||||
case MGPWireOp::BindSamplerStates:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindSamplerStates, "BindSamplerStates");
|
||||
return false;
|
||||
case MGPWireOp::SetShaderImages:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderImages, "SetShaderImages");
|
||||
return false;
|
||||
case MGPWireOp::SetShaderBuffers:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderBuffers, "SetShaderBuffers");
|
||||
return false;
|
||||
case MGPWireOp::SetStreamOutputTargets:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetStreamOutputTargets, "SetStreamOutputTargets");
|
||||
return false;
|
||||
case MGPWireOp::SetGlobalConstants:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetGlobalConstants, "SetGlobalConstants");
|
||||
return false;
|
||||
case MGPWireOp::SetVertexAttribDefaults:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexAttribDefaults, "SetVertexAttribDefaults");
|
||||
return false;
|
||||
case MGPWireOp::SetPixelPackState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPixelPackState, "SetPixelPackState");
|
||||
return false;
|
||||
case MGPWireOp::SetPatchState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPatchState, "SetPatchState");
|
||||
return false;
|
||||
case MGPWireOp::SetDrawProgram:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDrawProgram, "SetDrawProgram");
|
||||
return false;
|
||||
case MGPWireOp::SetDispatchProgram:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDispatchProgram, "SetDispatchProgram");
|
||||
return false;
|
||||
case MGPWireOp::SetResidualValueState:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetResidualValueState, "SetResidualValueState");
|
||||
return false;
|
||||
case MGPWireOp::SetTextureParams:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetTextureParams, "SetTextureParams");
|
||||
return false;
|
||||
case MGPWireOp::ResourceSubData:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubData, "ResourceSubData");
|
||||
return false;
|
||||
case MGPWireOp::BufferSubDataResident:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BufferSubDataResident, "BufferSubDataResident");
|
||||
return false;
|
||||
case MGPWireOp::ResourceSubDataComplete:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubDataComplete, "ResourceSubDataComplete");
|
||||
return false;
|
||||
case MGPWireOp::ResourceFlushRange:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceFlushRange, "ResourceFlushRange");
|
||||
return false;
|
||||
case MGPWireOp::ResourceReadback:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceReadback, "ResourceReadback");
|
||||
return false;
|
||||
case MGPWireOp::ResourceCopyRegion:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCopyRegion, "ResourceCopyRegion");
|
||||
return false;
|
||||
case MGPWireOp::GenerateMipmap:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GenerateMipmap, "GenerateMipmap");
|
||||
return false;
|
||||
case MGPWireOp::GetTextureImage:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImage, "GetTextureImage");
|
||||
return false;
|
||||
case MGPWireOp::Blit:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Blit, "Blit");
|
||||
return false;
|
||||
case MGPWireOp::Clear:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Clear, "Clear");
|
||||
return false;
|
||||
case MGPWireOp::ReadPixels:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ReadPixels, "ReadPixels");
|
||||
return false;
|
||||
case MGPWireOp::DrawVbo:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DrawVbo, "DrawVbo");
|
||||
return false;
|
||||
case MGPWireOp::LaunchGrid:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_LaunchGrid, "LaunchGrid");
|
||||
return false;
|
||||
case MGPWireOp::MemoryBarrier:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MemoryBarrier, "MemoryBarrier");
|
||||
return false;
|
||||
case MGPWireOp::BeginStreamOutput:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BeginStreamOutput, "BeginStreamOutput");
|
||||
return false;
|
||||
case MGPWireOp::EndStreamOutput:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_EndStreamOutput, "EndStreamOutput");
|
||||
return false;
|
||||
case MGPWireOp::PauseStreamOutput:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_PauseStreamOutput, "PauseStreamOutput");
|
||||
return false;
|
||||
case MGPWireOp::ResumeStreamOutput:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResumeStreamOutput, "ResumeStreamOutput");
|
||||
return false;
|
||||
case MGPWireOp::Flush:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Flush, "Flush");
|
||||
return false;
|
||||
case MGPWireOp::Present:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Present, "Present");
|
||||
return false;
|
||||
case MGPWireOp::SetSwapInterval:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSwapInterval, "SetSwapInterval");
|
||||
return false;
|
||||
case MGPWireOp::QueryTimestamp:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryTimestamp, "QueryTimestamp");
|
||||
return false;
|
||||
case MGPWireOp::QueryCounter:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCounter, "QueryCounter");
|
||||
return false;
|
||||
case MGPWireOp::FenceWaitServer:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWaitServer, "FenceWaitServer");
|
||||
return false;
|
||||
case MGPWireOp::kInvalid:
|
||||
case MGPWireOp::kOpCount:
|
||||
default:
|
||||
MGPipeWireProtocolFatal("<unknown opcode>", size, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
#undef MGP_WIRE_CHECK_BOUNDS
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Protocol/mg_protocol_base.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
|
||||
|
||||
// Shared vocabulary of the MG_Remote wire contracts (transport, framing, ring,
|
||||
// shm). Inherited from the earlier `Feat/CS-Delta-IPC` branch
|
||||
// (MobileGL/Protocol/mg_protocol_base.h) and cut down to what plan B's
|
||||
// transport actually needs: result codes, byte spans, a shm region reference
|
||||
// and the id typedefs.
|
||||
//
|
||||
// Deliberately NOT inherited: MobileGLObjectKind / MobileGLObjectScope /
|
||||
// MobileGLObjectHandle. Plan B does not put GL object identity on the wire at
|
||||
// all - the frontend allocates {slot, generation} handles in MG_Pipe
|
||||
// (PLAN-B.md section 4.2.1) and those are the only identity the backend ever
|
||||
// sees, so a second object-identity vocabulary here would be a drift surface
|
||||
// with no reader.
|
||||
//
|
||||
// This header must stay:
|
||||
// - pure C (compilable from C and C++, no MG C++ types, no exceptions/RTTI),
|
||||
// - dependency-free (only <stdbool.h>/<stddef.h>/<stdint.h>),
|
||||
// - append-only within an ABI major (see versioning rules below).
|
||||
//
|
||||
// Versioning rules (contract-wide):
|
||||
// - Every versioned struct starts with uint32_t structSize.
|
||||
// - Appending fields at the tail is a MINOR bump; receivers must ignore
|
||||
// bytes beyond the structSize they know.
|
||||
// - Changing/removing/reordering existing fields is a MAJOR bump.
|
||||
// - A major mismatch is a hard, structured failure, never an exception.
|
||||
// (Plan B keeps the structSize-first discipline as the answer to risk B-R10,
|
||||
// PLAN-B.md section 14.2.)
|
||||
|
||||
#ifndef MOBILEGL_REMOTE_PROTOCOL_BASE_H
|
||||
#define MOBILEGL_REMOTE_PROTOCOL_BASE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ABI versions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define MOBILEGL_PROTOCOL_ABI_MAJOR 1
|
||||
#define MOBILEGL_PROTOCOL_ABI_MINOR 0
|
||||
|
||||
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
|
||||
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
|
||||
#define MOBILEGL_ABI_MINOR_OF(version) ((uint32_t)(version) & 0xFFFFu)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ids
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef uint64_t MobileGLSessionId; // one client GL context flow
|
||||
typedef uint64_t MobileGLRequestSeq; // matches a request to its reply
|
||||
typedef uint32_t MobileGLSegmentId; // shm segment id within a connection
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spans / regions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Borrowed, read-only byte span. The pointee is owned by the producing side
|
||||
// and is only valid for the duration documented at the consuming call site.
|
||||
typedef struct MobileGLByteSpan {
|
||||
const void* data;
|
||||
uint64_t size;
|
||||
} MobileGLByteSpan;
|
||||
|
||||
typedef struct MobileGLMutableByteSpan {
|
||||
void* data;
|
||||
uint64_t size;
|
||||
} MobileGLMutableByteSpan;
|
||||
|
||||
// A byte range inside an already-established shm segment. Segments are
|
||||
// announced out of band (the SegmentRef table on the control channel, with the
|
||||
// fd itself passed by SCM_RIGHTS) and stay stable for their declared lifetime;
|
||||
// offsets are segment-relative.
|
||||
typedef struct MobileGLShmRegion {
|
||||
MobileGLSegmentId segmentId;
|
||||
uint32_t reserved;
|
||||
uint64_t offset;
|
||||
uint64_t size;
|
||||
} MobileGLShmRegion;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result codes (structured errors across every contract boundary)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
typedef enum MobileGLResult {
|
||||
MOBILEGL_OK = 0,
|
||||
MOBILEGL_ERR_NOT_INITIALIZED = 1,
|
||||
MOBILEGL_ERR_INVALID_ARGUMENT = 2,
|
||||
MOBILEGL_ERR_UNSUPPORTED = 3,
|
||||
MOBILEGL_ERR_OUT_OF_MEMORY = 4,
|
||||
MOBILEGL_ERR_PROTOCOL_MISMATCH = 5, // ABI/wire major mismatch, bad framing
|
||||
MOBILEGL_ERR_TRANSPORT_CLOSED = 6, // peer gone / EOF
|
||||
MOBILEGL_ERR_TIMEOUT = 7, // nothing arrived within the deadline
|
||||
MOBILEGL_ERR_SHM_EXHAUSTED = 8,
|
||||
MOBILEGL_ERR_SESSION_UNKNOWN = 9,
|
||||
MOBILEGL_ERR_HANDLE_UNKNOWN = 10,
|
||||
// The caller's buffer is smaller than the pending message. The message is
|
||||
// NOT consumed and the required size is reported back; see
|
||||
// ITransport::ReceiveFrame.
|
||||
MOBILEGL_ERR_BUFFER_TOO_SMALL = 11,
|
||||
MOBILEGL_ERR_FORCE_U32 = 0x7FFFFFFF
|
||||
} MobileGLResult;
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // MOBILEGL_REMOTE_PROTOCOL_BASE_H
|
||||
@@ -0,0 +1,235 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Protocol/protocol.fbs
|
||||
// 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
|
||||
|
||||
// MobileGL disaggregated wire protocol - CONTROL PLANE ONLY.
|
||||
//
|
||||
// Plan B (docs plan "MGPipe") section 8.1 inherits the transport design of the
|
||||
// earlier plan verbatim, and its section 7.1 splits the schema in two:
|
||||
//
|
||||
// - rare / variable-length / must-evolve messages -> FlatBuffers *tables*,
|
||||
// carried as complete framed messages over the control channel. That is
|
||||
// everything in this file.
|
||||
// - the hot path -> FlatBuffers *structs* (fixed layout, no vtable, no
|
||||
// offset indirection) written straight into the SEG_CMD ring. Those
|
||||
// records are generated from MG_Pipe/PipeCalls.def and are deliberately
|
||||
// NOT in this schema yet: the call catalogue is a separate P0 deliverable
|
||||
// and record numbering must never churn.
|
||||
//
|
||||
// Regeneration: scripts/gen_protocol.py (flatc is NOT part of the default
|
||||
// build graph). generated/protocol_generated.h is committed and CI's
|
||||
// flatc-check regenerates it and runs `git diff --exit-code`.
|
||||
|
||||
namespace MobileGL.Wire;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Segments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Segment layout is inherited unchanged (earlier plan section 6.1):
|
||||
// SEG_CMD 8MiB / SEG_STAGE 32MiB+ / SEG_REPLY 8MiB / SEG_EVENT 256KiB /
|
||||
// SEG_SHADOW[n] / SEG_ADOPT[n].
|
||||
enum SegmentKind : ubyte {
|
||||
None = 0,
|
||||
Cmd = 1, // client-owned command ring (RingControl + records)
|
||||
Stage = 2, // client-owned bulk staging
|
||||
Reply = 3, // server-owned reply pool
|
||||
Event = 4, // server-owned event ring
|
||||
Shadow = 5, // client-owned per-object shadow (P4.5+)
|
||||
Adopt = 6, // server-owned adopted store, client RW (>= 16MiB)
|
||||
}
|
||||
|
||||
// The fd itself never travels in a message: POSIX passes it with SCM_RIGHTS on
|
||||
// the aux socket (ITransport::ShareFd), Windows resolves `name`.
|
||||
table SegmentRef {
|
||||
id: uint;
|
||||
kind: SegmentKind;
|
||||
sizeBytes: ulong;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handshake
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
table Hello {
|
||||
abiMajor: uint;
|
||||
abiMinor: uint;
|
||||
buildFingerprint: string;
|
||||
backendType: uint;
|
||||
pid: uint;
|
||||
configBlob: [ubyte];
|
||||
}
|
||||
|
||||
table Welcome {
|
||||
abiMajor: uint;
|
||||
abiMinor: uint;
|
||||
serverPid: uint;
|
||||
cmdRing: SegmentRef;
|
||||
stageRing: SegmentRef;
|
||||
replyPool: SegmentRef;
|
||||
eventRing: SegmentRef;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Capabilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Replaces the 40 `pActiveBackendObject->` reads plus the 89 caps read sites
|
||||
// (plan B appendix A, `get_caps`). The three blobs are byte-for-byte images of
|
||||
// the corresponding POD structs; they are versioned by structSize-first
|
||||
// discipline, not by this schema.
|
||||
table CapsSnapshot {
|
||||
dynamicParameters: [ubyte];
|
||||
rendererInfo: [ubyte];
|
||||
formatCaps: [ubyte];
|
||||
extensions: [string];
|
||||
apiVersion: string;
|
||||
maxComputeWorkGroupCount: [int]; // 3 entries
|
||||
maxComputeWorkGroupSize: [int]; // 3 entries
|
||||
tableSlotMask: ulong; // which GLFunctionsTable slots the peer registered
|
||||
prefersCpuXfbPrimitiveAccounting: bool;
|
||||
}
|
||||
|
||||
table DefaultFramebufferInfo {
|
||||
width: int;
|
||||
height: int;
|
||||
colorFormat: uint;
|
||||
depthFormat: uint;
|
||||
stencilFormat: uint;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Surface / EGL lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum SurfaceOpKind : ubyte {
|
||||
None = 0,
|
||||
InitializeDisplay = 1,
|
||||
CreateWindowSurface = 2,
|
||||
CreatePbufferSurface = 3,
|
||||
ResizeWindowSurface = 4,
|
||||
ReleaseSurface = 5,
|
||||
MakeCurrent = 6,
|
||||
ReleaseCurrent = 7,
|
||||
}
|
||||
|
||||
enum WindowKind : ubyte {
|
||||
None = 0,
|
||||
AndroidNativeWindow = 1,
|
||||
X11 = 2,
|
||||
Win32Hwnd = 3,
|
||||
Surfaceless = 4,
|
||||
Pbuffer = 5,
|
||||
}
|
||||
|
||||
table SurfaceOp {
|
||||
seq: ulong;
|
||||
kind: SurfaceOpKind;
|
||||
display: ulong;
|
||||
surface: ulong;
|
||||
windowKind: WindowKind;
|
||||
nativeToken: ulong; // X11 XID / HWND; Android transfers the window out of band
|
||||
width: int;
|
||||
height: int;
|
||||
swapInterval: int;
|
||||
}
|
||||
|
||||
table SurfaceReply {
|
||||
seq: ulong;
|
||||
ok: bool;
|
||||
eglMajor: int;
|
||||
eglMinor: int;
|
||||
defaultFb: DefaultFramebufferInfo;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resync / aux / diagnostics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Sent by the client after it observes a serverEpoch bump (context lost or
|
||||
// server restart): every cached ring offset and every server-side object is
|
||||
// gone and the whole pushed state has to be replayed.
|
||||
table ResyncRequest {
|
||||
serverEpoch: uint;
|
||||
}
|
||||
|
||||
table ResyncDone {}
|
||||
|
||||
enum AuxRequestKind : ubyte {
|
||||
None = 0,
|
||||
FenceClientWait = 1,
|
||||
QueryResult = 2,
|
||||
ScalarGet = 3,
|
||||
}
|
||||
|
||||
// Requests issued from a thread that is not the ring producer (foreign-thread
|
||||
// sync / query polling), so they cannot take the SPSC ring.
|
||||
table AuxRequest {
|
||||
seq: ulong;
|
||||
kind: AuxRequestKind;
|
||||
payload: [ubyte];
|
||||
}
|
||||
|
||||
enum FatalCode : uint {
|
||||
None = 0,
|
||||
ProtocolCorruption = 1, // record bounds / self-describing length violated
|
||||
RingOverrun = 2,
|
||||
SegmentMismatch = 3,
|
||||
DeviceLost = 4,
|
||||
ServerCrashed = 5,
|
||||
AbiMismatch = 6,
|
||||
}
|
||||
|
||||
table Fatal {
|
||||
code: FatalCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Severity-graded per plan B section 8.2: <= Warn is lossy, >= Error is
|
||||
// lossless and rate limited.
|
||||
enum LogLevel : ubyte {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3,
|
||||
Fatal = 4,
|
||||
}
|
||||
|
||||
table LogLine {
|
||||
level: LogLevel;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Envelope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Union tags are wire values: only ever APPEND to this list.
|
||||
// ProgramReflection from the earlier plan's section 7.1 is intentionally
|
||||
// absent - plan B ships program artifacts inside the create_shader_state CSO
|
||||
// blob, so if a control-plane reflection message is ever needed it appends
|
||||
// here rather than reserving a tag today.
|
||||
union CtrlMsg {
|
||||
Hello,
|
||||
Welcome,
|
||||
CapsSnapshot,
|
||||
SurfaceOp,
|
||||
SurfaceReply,
|
||||
ResyncRequest,
|
||||
ResyncDone,
|
||||
AuxRequest,
|
||||
Fatal,
|
||||
LogLine,
|
||||
}
|
||||
|
||||
table CtrlEnvelope {
|
||||
msg: CtrlMsg;
|
||||
}
|
||||
|
||||
root_type CtrlEnvelope;
|
||||
file_identifier "MGLC";
|
||||
@@ -0,0 +1,259 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.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 "Doorbell.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <cerrno>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Same fallback as FdPassing.cpp: on macOS / BSD the protection is SO_NOSIGPIPE on the
|
||||
// socket, set in SocketDoorbell's constructor, not a per-send flag.
|
||||
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// CondVarDoorbell
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
struct CondVarDoorbell::Impl {
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
// Counted, not a flag: a wakeup that arrives while nobody is parked
|
||||
// must still be observed by the next Park.
|
||||
std::uint32_t signals = 0;
|
||||
};
|
||||
|
||||
CondVarDoorbell::CondVarDoorbell() : m_impl(new Impl()) {}
|
||||
|
||||
CondVarDoorbell::~CondVarDoorbell() { delete m_impl; }
|
||||
|
||||
void CondVarDoorbell::Notify() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
++m_impl->signals;
|
||||
}
|
||||
m_impl->cv.notify_one();
|
||||
}
|
||||
|
||||
bool CondVarDoorbell::Park(std::uint32_t timeoutMs) {
|
||||
std::unique_lock<std::mutex> lock(m_impl->mutex);
|
||||
// The death latch is tested under the same mutex Kill sets it under, so
|
||||
// a Kill cannot slip between this test and the wait below: it either
|
||||
// returns here or wakes the predicate.
|
||||
if (m_dead.load(std::memory_order_relaxed)) {
|
||||
return false;
|
||||
}
|
||||
if (m_impl->signals != 0) {
|
||||
--m_impl->signals;
|
||||
return true;
|
||||
}
|
||||
if (timeoutMs == 0) {
|
||||
return false;
|
||||
}
|
||||
const auto woken = [this] {
|
||||
return m_impl->signals != 0 || m_dead.load(std::memory_order_relaxed);
|
||||
};
|
||||
if (timeoutMs == kWaitForever) {
|
||||
m_impl->cv.wait(lock, woken);
|
||||
} else if (!m_impl->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), woken)) {
|
||||
return false;
|
||||
}
|
||||
if (m_dead.load(std::memory_order_relaxed)) {
|
||||
// Woken by Kill, not by an event. The caller re-tests its condition
|
||||
// regardless (Doorbell::Wait always does) and then sees Dead().
|
||||
return false;
|
||||
}
|
||||
--m_impl->signals;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CondVarDoorbell::Kill() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
m_dead.store(true, std::memory_order_release);
|
||||
}
|
||||
// notify_all, not notify_one: both a raw Park and a Doorbell::Wait may
|
||||
// be parked here, and after this nobody will ring again.
|
||||
m_impl->cv.notify_all();
|
||||
}
|
||||
|
||||
void CondVarDoorbell::Reset() {
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
m_impl->signals = 0;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SocketDoorbell
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
SocketDoorbell::SocketDoorbell(int fd, std::uint8_t code, bool ownsFd)
|
||||
: m_fd(fd), m_code(code), m_ownsFd(ownsFd) {
|
||||
#if defined(SO_NOSIGPIPE)
|
||||
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one:
|
||||
// a Notify to a hung-up peer must come back as EPIPE, not as a fatal signal.
|
||||
if (m_fd >= 0) {
|
||||
const int one = 1;
|
||||
(void)::setsockopt(m_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SocketDoorbell::~SocketDoorbell() {
|
||||
if (m_ownsFd && m_fd >= 0) {
|
||||
::close(m_fd);
|
||||
}
|
||||
}
|
||||
|
||||
void SocketDoorbell::Notify() {
|
||||
if (m_fd < 0) {
|
||||
return;
|
||||
}
|
||||
const std::uint8_t byte = m_code;
|
||||
for (;;) {
|
||||
const ssize_t written = ::send(m_fd, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (written == 1) {
|
||||
return;
|
||||
}
|
||||
if (written < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
// The socket buffer already holds unread wakeups: the peer has
|
||||
// one pending, which is all a doorbell promises.
|
||||
return;
|
||||
}
|
||||
if (written < 0 && (errno == EPIPE || errno == ECONNRESET)) {
|
||||
// The peer is gone: it can never ring back either, so latch it
|
||||
// here too rather than waiting for a Park to discover it.
|
||||
m_dead = true;
|
||||
return;
|
||||
}
|
||||
MGLOG_D("MG_Remote doorbell: send failed (errno=%d)", errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool SocketDoorbell::Park(std::uint32_t timeoutMs) {
|
||||
if (m_fd < 0 || m_dead) {
|
||||
return false;
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
int pollTimeout = -1;
|
||||
if (timeoutMs != kWaitForever) {
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
|
||||
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
|
||||
}
|
||||
struct pollfd pfd{};
|
||||
pfd.fd = m_fd;
|
||||
pfd.events = POLLIN;
|
||||
const int ready = ::poll(&pfd, 1, pollTimeout);
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue; // a signal is not a wakeup; keep the deadline
|
||||
}
|
||||
MGLOG_D("MG_Remote doorbell: poll failed (errno=%d)", errno);
|
||||
return false;
|
||||
}
|
||||
if (ready == 0) {
|
||||
return false; // timed out
|
||||
}
|
||||
// revents has to be inspected, not just `ready > 0`. Once the peer
|
||||
// closes its end the descriptor is permanently poll-ready with
|
||||
// nothing to read (measured on Linux: revents=POLLIN|POLLHUP,
|
||||
// recv()==0), so treating any readiness as a wakeup turns every
|
||||
// park on a dead peer into a 100% CPU spin - unbounded, because
|
||||
// Doorbell::Wait re-parks until its deadline and kWaitForever has
|
||||
// none.
|
||||
if ((pfd.revents & (POLLERR | POLLNVAL)) != 0) {
|
||||
MGLOG_D("MG_Remote doorbell: fd %d unusable (revents=0x%X)", m_fd,
|
||||
static_cast<unsigned>(pfd.revents));
|
||||
m_dead = true;
|
||||
return false;
|
||||
}
|
||||
if ((pfd.revents & POLLIN) != 0) {
|
||||
if (Drain() != 0) {
|
||||
return true; // a real wakeup byte
|
||||
}
|
||||
if (m_dead) {
|
||||
return false; // EOF, not an event
|
||||
}
|
||||
// Ready but empty and still alive: someone else drained it.
|
||||
// Report the wakeup and let the caller re-test its condition.
|
||||
return true;
|
||||
}
|
||||
if ((pfd.revents & POLLHUP) != 0) {
|
||||
m_dead = true;
|
||||
return false;
|
||||
}
|
||||
// Readiness with no bit we requested or recognise: there is
|
||||
// nothing to consume and no way to make progress, so refuse to
|
||||
// poll this descriptor again.
|
||||
MGLOG_D("MG_Remote doorbell: fd %d ready with revents=0x%X", m_fd,
|
||||
static_cast<unsigned>(pfd.revents));
|
||||
m_dead = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t SocketDoorbell::Drain() {
|
||||
// Level-triggered to edge-triggered: swallow every queued byte so one
|
||||
// stale wakeup cannot make later Parks return without an event.
|
||||
std::uint64_t consumed = 0;
|
||||
std::uint8_t scratch[64];
|
||||
for (;;) {
|
||||
const ssize_t got = ::recv(m_fd, scratch, sizeof(scratch), MSG_DONTWAIT);
|
||||
if (got > 0) {
|
||||
consumed += static_cast<std::uint64_t>(got);
|
||||
continue;
|
||||
}
|
||||
if (got == 0) {
|
||||
// Orderly shutdown on a stream socket: the peer is gone and
|
||||
// will never ring again.
|
||||
m_dead = true;
|
||||
return consumed;
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return consumed; // drained
|
||||
}
|
||||
MGLOG_D("MG_Remote doorbell: recv failed (errno=%d)", errno);
|
||||
m_dead = true;
|
||||
return consumed;
|
||||
}
|
||||
}
|
||||
|
||||
void SocketDoorbell::Reset() {
|
||||
if (m_fd < 0 || m_dead) {
|
||||
return;
|
||||
}
|
||||
(void)Drain();
|
||||
}
|
||||
|
||||
#endif // !_WIN32
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -0,0 +1,268 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.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 bidirectional doorbell: spin briefly, then park.
|
||||
//
|
||||
// Both directions exist, and that is the point (inherited design, earlier plan
|
||||
// section 6.2a):
|
||||
// - client -> server: the consumer spins, sets consumerParked, then blocks;
|
||||
// the producer rings only when consumerParked is set.
|
||||
// - server -> client: the client spins MOBILEGL_IPC_SPIN_US (default 50us),
|
||||
// sets producerParked, then blocks; the server rings after advancing any
|
||||
// watermark, only when producerParked is set.
|
||||
// Without the second direction every client wait - present credit, a blocking
|
||||
// kNeedsAck request, a full ring - degenerates into a cross-process spin on
|
||||
// one shared cache line: up to a whole frame of a big core at full clock on a
|
||||
// phone, fighting the GPU and the game's JVM for it. MobileGL has no affinity
|
||||
// control anywhere in the tree, so it cannot even be pushed to a little core.
|
||||
//
|
||||
// Two implementations, no platform-specific wakeup primitive (no futex, no
|
||||
// eventfd, no named event):
|
||||
// - CondVarDoorbell for `inproc` (one process, two threads),
|
||||
// - SocketDoorbell for `spawn` (one byte on a socket; POSIX only).
|
||||
//
|
||||
// The lost-wakeup window is closed by two seq_cst FENCES, not by the ordering
|
||||
// of the park flag's own load and store:
|
||||
// - the waiter sets the flag, executes std::atomic_thread_fence(seq_cst),
|
||||
// and THEN re-tests the condition (Doorbell::Wait);
|
||||
// - the notifier publishes its watermark, executes the same fence, and THEN
|
||||
// reads the flag (NotifyIfParked).
|
||||
// Both fences sit in the single seq_cst total order, so one precedes the
|
||||
// other, and [atomics.order] then forces at least one side to observe the
|
||||
// other's store. The flag's own accesses may be relaxed: they are not what
|
||||
// closes the window.
|
||||
//
|
||||
// A seq_cst store paired with a seq_cst load would NOT be enough, which is
|
||||
// why the fences are here and why neither may be removed. That Dekker
|
||||
// argument needs all FOUR accesses in the total order, and the other two are
|
||||
// not: the watermark publish is a release store (RingProducer::Publish) and
|
||||
// the condition re-test is an acquire load. On x86 the gap is concrete rather
|
||||
// than theoretical - a release store is a plain MOV that can still sit in the
|
||||
// store buffer while the load of the park flag, also a plain MOV, reads 0, so
|
||||
// the notifier skips the ring and the waiter parks on a stale watermark
|
||||
// forever. (ARMv8 survives it only because STLR->LDAR is RCsc, i.e. by luck.)
|
||||
//
|
||||
// The other half of the contract is ordering between the caller and the
|
||||
// fence: NotifyIfParked must be called AFTER the watermark is published. A
|
||||
// fence only orders what precedes it.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// MOBILEGL_IPC_SPIN_US default.
|
||||
inline constexpr std::uint32_t kDefaultSpinUs = 50;
|
||||
|
||||
// Park with no deadline.
|
||||
inline constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
|
||||
|
||||
// Wire codes, so a shared socket can carry both directions distinguishably.
|
||||
inline constexpr std::uint8_t kDoorbellRingAdvanced = 0x01; // client -> server
|
||||
inline constexpr std::uint8_t kDoorbellWatermarkAdvanced = 0x02; // server -> client
|
||||
|
||||
inline void CpuRelax() {
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
_mm_pause();
|
||||
#elif defined(__aarch64__) || defined(__arm__)
|
||||
__asm__ __volatile__("yield" ::: "memory");
|
||||
#else
|
||||
std::atomic_signal_fence(std::memory_order_seq_cst);
|
||||
#endif
|
||||
}
|
||||
|
||||
class Doorbell {
|
||||
public:
|
||||
virtual ~Doorbell() = default;
|
||||
|
||||
Doorbell(const Doorbell&) = delete;
|
||||
Doorbell& operator=(const Doorbell&) = delete;
|
||||
|
||||
// Wakes a parked peer. Cheap and idempotent: a wakeup that arrives when
|
||||
// nobody is parked is remembered, so the next Park returns immediately
|
||||
// rather than sleeping through an event that already happened.
|
||||
virtual void Notify() = 0;
|
||||
|
||||
// Blocks until notified or the deadline passes. Returns true when a
|
||||
// wakeup was consumed. timeoutMs == 0 polls; kWaitForever never times
|
||||
// out.
|
||||
virtual bool Park(std::uint32_t timeoutMs) = 0;
|
||||
|
||||
// Drops pending wakeups. Used when a waiter gives up, so a stale byte
|
||||
// does not make the next Park return spuriously forever.
|
||||
virtual void Reset() = 0;
|
||||
|
||||
// True once the wakeup channel is permanently unusable: the peer closed
|
||||
// its end of the socket, or the inproc channel was shut down. A dead
|
||||
// doorbell can never deliver another wakeup, and Wait must stop
|
||||
// re-parking on it - for the socket because its descriptor is
|
||||
// permanently poll-ready and a waiter with no deadline would burn a
|
||||
// big core at full clock, for the condvar because Park would otherwise
|
||||
// block forever and Shutdown could never join the waiter. Every
|
||||
// implementation has a death state; the base default is only for a
|
||||
// bell that cannot die.
|
||||
virtual bool Dead() const { return false; }
|
||||
|
||||
// Spin `spinUs`, then park until `ready()` or the deadline.
|
||||
// `parked` is the RingControl flag the peer tests before ringing.
|
||||
template <class Ready>
|
||||
bool Wait(std::atomic<std::uint32_t>& parked, Ready&& ready, std::uint32_t spinUs,
|
||||
std::uint32_t timeoutMs) {
|
||||
if (ready()) {
|
||||
return true;
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto deadline = timeoutMs == kWaitForever
|
||||
? std::chrono::steady_clock::time_point::max()
|
||||
: start + std::chrono::milliseconds(timeoutMs);
|
||||
|
||||
const auto spinEnd = start + std::chrono::microseconds(spinUs);
|
||||
while (std::chrono::steady_clock::now() < spinEnd) {
|
||||
if (ready()) {
|
||||
return true;
|
||||
}
|
||||
CpuRelax();
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// Announce, FENCE, then re-test. The fence is the mechanism -
|
||||
// see the file header - so setting the flag itself is relaxed.
|
||||
parked.store(1, std::memory_order_relaxed);
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
if (ready()) {
|
||||
parked.store(0, std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
parked.store(0, std::memory_order_relaxed);
|
||||
return ready();
|
||||
}
|
||||
std::uint32_t chunkMs = kWaitForever;
|
||||
if (timeoutMs != kWaitForever) {
|
||||
const auto remaining =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count();
|
||||
chunkMs = remaining <= 0 ? 0 : static_cast<std::uint32_t>(remaining);
|
||||
}
|
||||
Park(chunkMs);
|
||||
// Clearing is relaxed on purpose: a notifier that reads a
|
||||
// stale 1 only rings a bell nobody is waiting on, which the
|
||||
// doorbell remembers and the next Park consumes. The dangerous
|
||||
// direction - a notifier reading 0 while the waiter is really
|
||||
// parked - is the one the fence above rules out.
|
||||
parked.store(0, std::memory_order_relaxed);
|
||||
if (ready()) {
|
||||
return true;
|
||||
}
|
||||
if (Dead()) {
|
||||
// Nothing can ring this bell again and parking on it no
|
||||
// longer blocks, so looping here would spin at full clock
|
||||
// for as long as the caller is willing to wait - which,
|
||||
// with kWaitForever, is forever.
|
||||
return false;
|
||||
}
|
||||
if (timeoutMs != kWaitForever && std::chrono::steady_clock::now() >= deadline) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Doorbell() = default;
|
||||
};
|
||||
|
||||
// Rings `bell` only when the peer said it is parked.
|
||||
//
|
||||
// PRECONDITION: whatever the waiter's condition reads - the ring head, a
|
||||
// sequence watermark, a queue push - is ALREADY published when this is
|
||||
// called. The fence only orders what precedes it, so ringing before
|
||||
// publishing reopens the window this closes. The fence pairs with the one
|
||||
// in Doorbell::Wait; see the file header for why the flag's own memory
|
||||
// order is not what makes this sound.
|
||||
inline void NotifyIfParked(Doorbell& bell, std::atomic<std::uint32_t>& parked) {
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
if (parked.load(std::memory_order_relaxed) != 0) {
|
||||
bell.Notify();
|
||||
}
|
||||
}
|
||||
|
||||
// `inproc`: one process, two threads.
|
||||
class CondVarDoorbell final : public Doorbell {
|
||||
public:
|
||||
CondVarDoorbell();
|
||||
~CondVarDoorbell() override;
|
||||
|
||||
void Notify() override;
|
||||
bool Park(std::uint32_t timeoutMs) override;
|
||||
void Reset() override;
|
||||
bool Dead() const override { return m_dead.load(std::memory_order_acquire); }
|
||||
|
||||
// Hangs the bell up for good: every parked waiter returns false now and
|
||||
// every later Park returns false at once. The inproc twin of the socket
|
||||
// peer closing its end (SocketDoorbell latches m_dead on EOF), and what
|
||||
// InProcessChannel::Close rings instead of Notify. A Notify is consumed
|
||||
// by ONE Park; Doorbell::Wait then re-tests its condition, finds
|
||||
// nothing published, finds the bell alive, and with kWaitForever parks
|
||||
// again - so a Shutdown that only rang could never join a server thread
|
||||
// sitting in the design's own steady state (spun, set consumerParked,
|
||||
// blocked). Irreversible by design, like the socket's.
|
||||
void Kill();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl* m_impl;
|
||||
std::atomic<bool> m_dead{false};
|
||||
};
|
||||
|
||||
#if !defined(_WIN32)
|
||||
// `spawn`: one byte on a socket (one direction of a socketpair, or the aux
|
||||
// socket). POSIX only; the Windows path will use an overlapped named pipe
|
||||
// and is not part of this skeleton.
|
||||
class SocketDoorbell final : public Doorbell {
|
||||
public:
|
||||
// `fd` must be one end of an AF_UNIX socket pair, not a pipe: Notify
|
||||
// uses send() with MSG_DONTWAIT|MSG_NOSIGNAL and Park uses
|
||||
// poll()+recv(), which a pipe end refuses with ENOTSOCK. Prefer
|
||||
// SOCK_STREAM for the spawn transport - measured on Linux, a closed
|
||||
// peer makes a stream end report POLLIN|POLLHUP with recv()==0, which
|
||||
// is how death is detected, while a SOCK_DGRAM end reports no
|
||||
// readiness at all and a waiter with no deadline would simply hang.
|
||||
// When `ownsFd` the descriptor is closed with this object. `code` is
|
||||
// the byte written by Notify.
|
||||
SocketDoorbell(int fd, std::uint8_t code, bool ownsFd);
|
||||
~SocketDoorbell() override;
|
||||
|
||||
void Notify() override;
|
||||
bool Park(std::uint32_t timeoutMs) override;
|
||||
void Reset() override;
|
||||
bool Dead() const override { return m_dead; }
|
||||
|
||||
int Fd() const { return m_fd; }
|
||||
|
||||
private:
|
||||
// Consumes every queued wakeup byte and returns how many. Latches
|
||||
// m_dead on EOF: recv returning 0 on a stream socket is the peer's
|
||||
// hangup, not a wakeup, and the descriptor stays poll-ready forever
|
||||
// afterwards.
|
||||
std::uint64_t Drain();
|
||||
|
||||
int m_fd;
|
||||
std::uint8_t m_code;
|
||||
bool m_ownsFd;
|
||||
bool m_dead = false;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -0,0 +1,323 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.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 "FdPassing.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// MSG_NOSIGNAL is Linux (and Android). macOS and the BSDs spell the same protection as the
|
||||
// SO_NOSIGPIPE socket option, set once per socket at creation (CreateSocketPair below, and
|
||||
// SocketDoorbell's constructor). With neither, a write to a hung-up peer raises SIGPIPE and
|
||||
// kills the process instead of returning EPIPE.
|
||||
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport::FdPassing {
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
bool Supported() { return false; }
|
||||
|
||||
MobileGLResult CreateSocketPair(int[2]) { return MOBILEGL_ERR_UNSUPPORTED; }
|
||||
|
||||
MobileGLResult SendFd(int, int, MobileGLByteSpan) { return MOBILEGL_ERR_UNSUPPORTED; }
|
||||
|
||||
MobileGLResult ReceiveFd(int, int*, MobileGLMutableByteSpan, std::uint64_t*, std::uint32_t) {
|
||||
return MOBILEGL_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
// Every datagram starts with this, so the sideband length is explicit
|
||||
// and a stray datagram is recognisable.
|
||||
struct SidebandHeader {
|
||||
std::uint32_t magic;
|
||||
std::uint32_t sidebandSize;
|
||||
};
|
||||
constexpr std::uint32_t kSidebandMagic = 0x4446474Du; // 'MGFD' on the wire
|
||||
|
||||
int WaitReadable(int socket, std::uint32_t timeoutMs) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
int pollTimeout = -1;
|
||||
if (timeoutMs != 0xFFFFFFFFu) {
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
|
||||
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
|
||||
}
|
||||
struct pollfd pfd{};
|
||||
pfd.fd = socket;
|
||||
pfd.events = POLLIN;
|
||||
const int ready = ::poll(&pfd, 1, pollTimeout);
|
||||
if (ready < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Supported() { return true; }
|
||||
|
||||
MobileGLResult CreateSocketPair(int outFds[2]) {
|
||||
if (outFds == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
int fds[2] = {-1, -1};
|
||||
int type = SOCK_DGRAM;
|
||||
#if defined(SOCK_CLOEXEC)
|
||||
type |= SOCK_CLOEXEC;
|
||||
#endif
|
||||
if (::socketpair(AF_UNIX, type, 0, fds) != 0) {
|
||||
MGLOG_E("MG_Remote fd passing: socketpair failed (errno=%d)", errno);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
#if defined(SO_NOSIGPIPE)
|
||||
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one.
|
||||
for (int fd : fds) {
|
||||
const int one = 1;
|
||||
(void)::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
|
||||
}
|
||||
#endif
|
||||
outFds[0] = fds[0];
|
||||
outFds[1] = fds[1];
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband) {
|
||||
if (socket < 0 || fd < 0) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (sideband.size > kMaxSidebandBytes || (sideband.size != 0 && sideband.data == nullptr)) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
|
||||
SidebandHeader header{};
|
||||
header.magic = kSidebandMagic;
|
||||
header.sidebandSize = static_cast<std::uint32_t>(sideband.size);
|
||||
std::memcpy(payload, &header, sizeof(header));
|
||||
if (sideband.size != 0) {
|
||||
std::memcpy(payload + sizeof(header), sideband.data,
|
||||
static_cast<std::size_t>(sideband.size));
|
||||
}
|
||||
const std::size_t payloadSize = sizeof(header) + static_cast<std::size_t>(sideband.size);
|
||||
|
||||
struct iovec iov{};
|
||||
iov.iov_base = payload;
|
||||
iov.iov_len = payloadSize;
|
||||
|
||||
// CMSG_SPACE, not sizeof: the control buffer has to hold the aligned
|
||||
// cmsghdr as well as the descriptor.
|
||||
union {
|
||||
struct cmsghdr align;
|
||||
char bytes[CMSG_SPACE(sizeof(int))];
|
||||
} control{};
|
||||
std::memset(&control, 0, sizeof(control));
|
||||
|
||||
struct msghdr msg{};
|
||||
msg.msg_iov = &iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = control.bytes;
|
||||
msg.msg_controllen = sizeof(control.bytes);
|
||||
|
||||
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
|
||||
cmsg->cmsg_level = SOL_SOCKET;
|
||||
cmsg->cmsg_type = SCM_RIGHTS;
|
||||
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
|
||||
std::memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
|
||||
|
||||
for (;;) {
|
||||
const ssize_t sent = ::sendmsg(socket, &msg, MSG_NOSIGNAL);
|
||||
if (sent >= 0) {
|
||||
if (static_cast<std::size_t>(sent) != payloadSize) {
|
||||
// A datagram socket sends all or nothing.
|
||||
MGLOG_E("MG_Remote fd passing: short datagram (%zd of %zu bytes)", sent,
|
||||
payloadSize);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EPIPE || errno == ECONNRESET) {
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
MGLOG_E("MG_Remote fd passing: sendmsg failed (errno=%d)", errno);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
|
||||
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) {
|
||||
if (socket < 0 || outFd == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
*outFd = -1;
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = 0;
|
||||
}
|
||||
// Checked before the recvmsg: a datagram cannot be partially consumed,
|
||||
// so a too-small destination must never cost us the descriptor.
|
||||
if (sideband.size < kMaxSidebandBytes) {
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = kMaxSidebandBytes;
|
||||
}
|
||||
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
if (sideband.data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const int ready = WaitReadable(socket, timeoutMs);
|
||||
if (ready < 0) {
|
||||
MGLOG_E("MG_Remote fd passing: poll failed (errno=%d)", errno);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
if (ready == 0) {
|
||||
return MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
|
||||
struct iovec iov{};
|
||||
iov.iov_base = payload;
|
||||
iov.iov_len = sizeof(payload);
|
||||
|
||||
union {
|
||||
struct cmsghdr align;
|
||||
char bytes[CMSG_SPACE(sizeof(int) * 4)];
|
||||
} control{};
|
||||
std::memset(&control, 0, sizeof(control));
|
||||
|
||||
struct msghdr msg{};
|
||||
msg.msg_iov = &iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = control.bytes;
|
||||
msg.msg_controllen = sizeof(control.bytes);
|
||||
|
||||
ssize_t got = 0;
|
||||
for (;;) {
|
||||
int flags = 0;
|
||||
#if defined(MSG_CMSG_CLOEXEC)
|
||||
flags |= MSG_CMSG_CLOEXEC;
|
||||
#endif
|
||||
got = ::recvmsg(socket, &msg, flags);
|
||||
if (got >= 0) {
|
||||
break;
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == ECONNRESET) {
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
MGLOG_E("MG_Remote fd passing: recvmsg failed (errno=%d)", errno);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
if (got == 0) {
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
|
||||
// Collect every descriptor first, so an unexpected extra one is closed
|
||||
// rather than leaked, whatever else is wrong with the message.
|
||||
int received[4];
|
||||
int receivedCount = 0;
|
||||
for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); cmsg != nullptr;
|
||||
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
|
||||
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
|
||||
continue;
|
||||
}
|
||||
const std::size_t bytes = cmsg->cmsg_len - CMSG_LEN(0);
|
||||
const int count = static_cast<int>(bytes / sizeof(int));
|
||||
for (int i = 0; i < count && receivedCount < 4; ++i) {
|
||||
int fd = -1;
|
||||
std::memcpy(&fd, CMSG_DATA(cmsg) + i * sizeof(int), sizeof(fd));
|
||||
received[receivedCount++] = fd;
|
||||
}
|
||||
}
|
||||
#if !defined(MSG_CMSG_CLOEXEC)
|
||||
// No atomic close-on-exec on receive here (macOS, the BSDs): set it by hand on every
|
||||
// descriptor that arrived, before anything else can fork. The window between the
|
||||
// recvmsg and this loop is the platform's, not ours; leaving the flag off altogether
|
||||
// would hand every shared segment to every child the process ever spawns.
|
||||
for (int i = 0; i < receivedCount; ++i) {
|
||||
if (received[i] >= 0) {
|
||||
(void)::fcntl(received[i], F_SETFD, FD_CLOEXEC);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const auto closeAll = [&](int keepIndex) {
|
||||
for (int i = 0; i < receivedCount; ++i) {
|
||||
if (i != keepIndex && received[i] >= 0) {
|
||||
::close(received[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ((msg.msg_flags & MSG_CTRUNC) != 0) {
|
||||
// The kernel dropped ancillary data: whatever arrived is not a
|
||||
// complete offer, and silently continuing would hand the caller a
|
||||
// half-transferred segment.
|
||||
MGLOG_E("MG_Remote fd passing: ancillary data truncated; the descriptor did not "
|
||||
"arrive intact");
|
||||
closeAll(-1);
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (receivedCount != 1) {
|
||||
MGLOG_E("MG_Remote fd passing: expected exactly one descriptor, got %d", receivedCount);
|
||||
closeAll(-1);
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (static_cast<std::size_t>(got) < sizeof(SidebandHeader)) {
|
||||
MGLOG_E("MG_Remote fd passing: %zd byte datagram is shorter than the header", got);
|
||||
closeAll(-1);
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
|
||||
SidebandHeader header{};
|
||||
std::memcpy(&header, payload, sizeof(header));
|
||||
if (header.magic != kSidebandMagic ||
|
||||
header.sidebandSize > kMaxSidebandBytes ||
|
||||
sizeof(SidebandHeader) + header.sidebandSize != static_cast<std::size_t>(got)) {
|
||||
MGLOG_E("MG_Remote fd passing: bad sideband header (magic=0x%08X size=%u datagram=%zd)",
|
||||
header.magic, header.sidebandSize, got);
|
||||
closeAll(-1);
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
|
||||
if (header.sidebandSize != 0) {
|
||||
std::memcpy(sideband.data, payload + sizeof(SidebandHeader), header.sidebandSize);
|
||||
}
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = header.sidebandSize;
|
||||
}
|
||||
*outFd = received[0];
|
||||
closeAll(0);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport::FdPassing
|
||||
@@ -0,0 +1,67 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.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
|
||||
|
||||
// SCM_RIGHTS descriptor passing over an AF_UNIX socket pair. POSIX only.
|
||||
//
|
||||
// This is the FIRST transport commit, deliberately (inherited design, plan
|
||||
// section 8.1, "SCM_RIGHTS must be implemented in the first transport
|
||||
// commit"). The earlier branch pushed it to a later phase and hardcoded
|
||||
// `out->fd = -1` in its offer poll, so on the only platform that matters its
|
||||
// data plane could never move a byte: every segment announcement resolved to
|
||||
// "no descriptor". A transport whose shm cannot cross the process boundary is
|
||||
// not a transport.
|
||||
//
|
||||
// Channel shape: a dedicated AF_UNIX SOCK_DGRAM socketpair, NOT the control
|
||||
// byte stream. Two reasons:
|
||||
// - SOCK_DGRAM preserves message boundaries on every POSIX (SOCK_SEQPACKET
|
||||
// does not exist on macOS), so one sendmsg is exactly one recvmsg and the
|
||||
// ancillary data can never be split away from its payload;
|
||||
// - ancillary data attached to a byte stream binds to whichever ordinary
|
||||
// byte happens to be at the front of the reader's buffer, which is
|
||||
// unmanageable once frames are being reassembled.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Protocol/mg_protocol_base.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport::FdPassing {
|
||||
|
||||
// Upper bound for the bytes that travel with a descriptor (a SegmentRef
|
||||
// sized announcement, not payload).
|
||||
inline constexpr std::uint64_t kMaxSidebandBytes = 256;
|
||||
|
||||
// False on platforms without SCM_RIGHTS (Windows).
|
||||
bool Supported();
|
||||
|
||||
// Creates the aux socket pair. Both descriptors are CLOEXEC and owned by
|
||||
// the caller. outFds[0] is conventionally the client end, [1] the server's
|
||||
// (the one that is inherited or passed to the spawned process).
|
||||
MobileGLResult CreateSocketPair(int outFds[2]);
|
||||
|
||||
// Sends `fd` with `sideband` attached. The caller keeps ownership of `fd`
|
||||
// (the peer gets its own descriptor for the same open file description).
|
||||
// sideband.size must be <= kMaxSidebandBytes.
|
||||
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband);
|
||||
|
||||
// Receives one descriptor and its sideband bytes.
|
||||
//
|
||||
// `sideband` must be at least kMaxSidebandBytes: a datagram cannot be
|
||||
// partially consumed, so the capacity is checked BEFORE anything is read.
|
||||
// A short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with
|
||||
// *outSidebandSize = kMaxSidebandBytes and consumes nothing, so no
|
||||
// descriptor is ever dropped on the floor.
|
||||
//
|
||||
// On success *outFd owns a descriptor this process must close.
|
||||
// MOBILEGL_ERR_TIMEOUT when nothing arrived (timeoutMs 0 = poll),
|
||||
// MOBILEGL_ERR_TRANSPORT_CLOSED on peer close.
|
||||
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
|
||||
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs);
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport::FdPassing
|
||||
@@ -0,0 +1,208 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/Framing.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
|
||||
|
||||
// Control-channel wire framing: [u32 magic 'MGLF'][u32 payloadLength][payload].
|
||||
// Length excludes the 8-byte header and is capped at 64 MiB.
|
||||
//
|
||||
// Two defects of the earlier branch's codec are fixed here, and both are the
|
||||
// reason this file is not a copy of it:
|
||||
//
|
||||
// 1. Its Feed() unconditionally returned OK and its header peek merely
|
||||
// returned false on a bad magic or an oversized length. A corrupt or
|
||||
// desynchronized stream therefore turned into a silent, permanent hang -
|
||||
// the reader kept waiting for a message that could never be parsed, with
|
||||
// no error anywhere. Here a violation latches a failed state, is logged at
|
||||
// ERROR, and every later call returns MOBILEGL_ERR_PROTOCOL_MISMATCH.
|
||||
//
|
||||
// 2. Its receive path failed the call and consumed the message when the
|
||||
// caller's buffer was too small, wedging the stream. Here
|
||||
// MOBILEGL_ERR_BUFFER_TOO_SMALL reports the required size and KEEPS the
|
||||
// message queued.
|
||||
//
|
||||
// The reader is a plain byte-stream reassembler: it never assumes a read()
|
||||
// returned a whole frame.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Protocol/mg_protocol_base.h"
|
||||
|
||||
// NOT <MG_Util/Debug/Log.h>: that header pulls the GL frontend's umbrella into
|
||||
// every translation unit that reassembles a frame. See WireLog.h.
|
||||
#include "WireLog.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// 'MGLF', little-endian on the wire (both ends are the same machine).
|
||||
inline constexpr std::uint32_t kFrameMagic = 0x464C474Du;
|
||||
inline constexpr std::uint64_t kFrameHeaderSize = 8;
|
||||
inline constexpr std::uint64_t kMaxFramePayloadSize = 64ull * 1024 * 1024;
|
||||
|
||||
// Compaction threshold: consumed bytes are dropped from the front once
|
||||
// enough of them accumulate, so a long-lived reader neither memmoves per
|
||||
// message nor grows without bound.
|
||||
inline constexpr std::uint64_t kFrameReaderCompactThreshold = 64ull * 1024;
|
||||
|
||||
// Appends one framed message to `out`.
|
||||
inline MobileGLResult AppendFrame(std::vector<std::uint8_t>& out, const void* payload,
|
||||
std::uint64_t size) {
|
||||
if (size > kMaxFramePayloadSize) {
|
||||
WireLogError("MG_Remote framing: refusing to send a %llu byte payload (cap %llu); "
|
||||
"bulk bytes belong in shm",
|
||||
static_cast<unsigned long long>(size),
|
||||
static_cast<unsigned long long>(kMaxFramePayloadSize));
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (size != 0 && payload == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::uint8_t header[kFrameHeaderSize];
|
||||
const std::uint32_t magic = kFrameMagic;
|
||||
const std::uint32_t length = static_cast<std::uint32_t>(size);
|
||||
std::memcpy(header + 0, &magic, sizeof(magic));
|
||||
std::memcpy(header + 4, &length, sizeof(length));
|
||||
out.insert(out.end(), header, header + kFrameHeaderSize);
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(payload);
|
||||
out.insert(out.end(), bytes, bytes + size);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
// Incremental frame extractor over a raw byte stream.
|
||||
class FrameReader {
|
||||
public:
|
||||
// Feeds raw stream bytes. Validates the frame header the moment enough
|
||||
// bytes for one exist - a bad magic or an oversized length is reported
|
||||
// here, not swallowed.
|
||||
MobileGLResult Feed(const void* data, std::uint64_t size) {
|
||||
if (m_failed) {
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (size != 0) {
|
||||
if (data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
m_buffer.insert(m_buffer.end(), bytes, bytes + size);
|
||||
}
|
||||
return ParseHeader();
|
||||
}
|
||||
|
||||
bool Failed() const { return m_failed; }
|
||||
|
||||
bool HasMessage() const {
|
||||
return !m_failed && m_haveHeader && Available() >= kFrameHeaderSize + m_pendingSize;
|
||||
}
|
||||
|
||||
// Size of the next complete message, or 0 when none is complete yet.
|
||||
std::uint64_t PendingMessageSize() const { return HasMessage() ? m_pendingSize : 0; }
|
||||
|
||||
std::uint64_t BufferedBytes() const { return Available(); }
|
||||
|
||||
// Copies the next complete message out.
|
||||
// MOBILEGL_OK - copied, *outSize set, message consumed
|
||||
// MOBILEGL_ERR_BUFFER_TOO_SMALL - *outSize = required size, message KEPT
|
||||
// MOBILEGL_ERR_TIMEOUT - no complete message buffered
|
||||
// MOBILEGL_ERR_PROTOCOL_MISMATCH- the stream is latched failed
|
||||
MobileGLResult TakeMessage(MobileGLMutableByteSpan buffer, std::uint64_t* outSize) {
|
||||
if (m_failed) {
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (!HasMessage()) {
|
||||
return MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
if (outSize != nullptr) {
|
||||
*outSize = m_pendingSize;
|
||||
}
|
||||
if (buffer.size < m_pendingSize) {
|
||||
// The message stays queued; the caller retries with a big
|
||||
// enough buffer.
|
||||
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
if (m_pendingSize != 0) {
|
||||
if (buffer.data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer.data, m_buffer.data() + m_readPos + kFrameHeaderSize,
|
||||
static_cast<std::size_t>(m_pendingSize));
|
||||
}
|
||||
Consume();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
// Convenience overload that sizes the destination itself.
|
||||
MobileGLResult TakeMessage(std::vector<std::uint8_t>& out) {
|
||||
if (m_failed) {
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (!HasMessage()) {
|
||||
return MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
const auto* first = m_buffer.data() + m_readPos + kFrameHeaderSize;
|
||||
out.assign(first, first + m_pendingSize);
|
||||
Consume();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint64_t Available() const { return m_buffer.size() - m_readPos; }
|
||||
|
||||
MobileGLResult ParseHeader() {
|
||||
if (m_haveHeader || Available() < kFrameHeaderSize) {
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
std::uint32_t magic = 0;
|
||||
std::uint32_t length = 0;
|
||||
std::memcpy(&magic, m_buffer.data() + m_readPos, sizeof(magic));
|
||||
std::memcpy(&length, m_buffer.data() + m_readPos + 4, sizeof(length));
|
||||
if (magic != kFrameMagic) {
|
||||
m_failed = true;
|
||||
WireLogError("MG_Remote framing: bad frame magic 0x%08X (expected 0x%08X); the "
|
||||
"control stream is desynchronized and this transport is now dead",
|
||||
magic, kFrameMagic);
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
if (length > kMaxFramePayloadSize) {
|
||||
m_failed = true;
|
||||
WireLogError("MG_Remote framing: frame length %u exceeds the %llu byte cap; "
|
||||
"refusing to allocate on a peer-supplied length",
|
||||
length, static_cast<unsigned long long>(kMaxFramePayloadSize));
|
||||
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
|
||||
}
|
||||
m_pendingSize = length;
|
||||
m_haveHeader = true;
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
void Consume() {
|
||||
m_readPos += kFrameHeaderSize + m_pendingSize;
|
||||
m_pendingSize = 0;
|
||||
m_haveHeader = false;
|
||||
if (m_readPos == m_buffer.size()) {
|
||||
m_buffer.clear();
|
||||
m_readPos = 0;
|
||||
} else if (m_readPos >= kFrameReaderCompactThreshold) {
|
||||
m_buffer.erase(m_buffer.begin(),
|
||||
m_buffer.begin() + static_cast<std::ptrdiff_t>(m_readPos));
|
||||
m_readPos = 0;
|
||||
}
|
||||
// Header of the next message may already be buffered.
|
||||
(void)ParseHeader();
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> m_buffer;
|
||||
std::uint64_t m_readPos = 0;
|
||||
std::uint64_t m_pendingSize = 0;
|
||||
bool m_haveHeader = false;
|
||||
bool m_failed = false;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -0,0 +1,130 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/ITransport.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 control-plane transport interface.
|
||||
//
|
||||
// It is deliberately dumb: complete messages in, complete messages out, plus
|
||||
// the one thing shared memory cannot do without help - handing a file
|
||||
// descriptor to the peer. No session routing, no seq accounting, no
|
||||
// serialization; those live above, in the protocol layer.
|
||||
//
|
||||
// Everything on the hot path bypasses this interface entirely: records go into
|
||||
// the SEG_CMD ring (Ring.h) and the peer is woken through a Doorbell
|
||||
// (Doorbell.h). ITransport carries the handshake, surface ops, resync, aux
|
||||
// requests and fatals - the rare, variable-length, must-evolve traffic that
|
||||
// plan section 7.1 assigns to FlatBuffers tables.
|
||||
//
|
||||
// This header stays dependency-light on purpose (mg_protocol_base.h plus the
|
||||
// standard library): it is included by both roles and by the eventual
|
||||
// server-side binary, and nothing about a byte pipe needs the GL frontend's
|
||||
// umbrella header.
|
||||
//
|
||||
// Threading: one instance is not internally synchronized for send; callers
|
||||
// serialize sends. ReceiveFrame/ReceiveFd may be called from one dedicated
|
||||
// reader thread concurrently with sends from another.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Protocol/mg_protocol_base.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// Which end of the connection this instance is.
|
||||
enum class TransportRole : std::uint32_t {
|
||||
Server = 1, // accepts the client connection
|
||||
Client = 2, // connects to the server endpoint
|
||||
InProcess = 3, // same-process hand-off (CI / inproc delivery mode)
|
||||
};
|
||||
|
||||
class ITransport {
|
||||
public:
|
||||
virtual ~ITransport() = default;
|
||||
|
||||
ITransport(const ITransport&) = delete;
|
||||
ITransport& operator=(const ITransport&) = delete;
|
||||
|
||||
// ---- control plane -------------------------------------------------
|
||||
|
||||
// Sends one complete message. `bytes` is borrowed: the implementation
|
||||
// either copies it or completes the underlying write before returning.
|
||||
// A payload larger than Framing::kMaxFramePayloadSize is rejected with
|
||||
// MOBILEGL_ERR_INVALID_ARGUMENT - bulk bytes belong in shm, never here.
|
||||
virtual MobileGLResult SendFrame(MobileGLByteSpan bytes) = 0;
|
||||
|
||||
// Receives the next complete message.
|
||||
//
|
||||
// MOBILEGL_OK - copied into `buffer`, *outSize is
|
||||
// the message size, message consumed.
|
||||
// MOBILEGL_ERR_BUFFER_TOO_SMALL - `buffer` is too small. *outSize is
|
||||
// the size required and THE MESSAGE
|
||||
// STAYS QUEUED: call again with a
|
||||
// buffer of at least that size and it
|
||||
// is still there.
|
||||
// MOBILEGL_ERR_TIMEOUT - nothing arrived within timeoutMs
|
||||
// (0 = non-blocking poll).
|
||||
// MOBILEGL_ERR_TRANSPORT_CLOSED - peer gone, nothing left buffered.
|
||||
// MOBILEGL_ERR_PROTOCOL_MISMATCH- framing violated; the transport is
|
||||
// latched failed and never recovers.
|
||||
//
|
||||
// The buffer-too-small half of that contract is the whole point of
|
||||
// having one: the earlier branch's transport failed the call AND
|
||||
// dropped the message, which wedges the stream permanently the first
|
||||
// time a message is bigger than the reader's guess.
|
||||
virtual MobileGLResult ReceiveFrame(MobileGLMutableByteSpan buffer, std::uint64_t* outSize,
|
||||
std::uint32_t timeoutMs) = 0;
|
||||
|
||||
// Size of the next pending message, or 0 when none is buffered. Lets a
|
||||
// caller size its buffer without a failed receive first.
|
||||
virtual std::uint64_t PeekFrameSize() = 0;
|
||||
|
||||
// ---- descriptor passing --------------------------------------------
|
||||
|
||||
// Hands `fd` to the peer. POSIX: SCM_RIGHTS over the aux socket (see
|
||||
// FdPassing.h). Windows: not applicable, returns
|
||||
// MOBILEGL_ERR_UNSUPPORTED - the section name travels inside SegmentRef
|
||||
// instead. The caller keeps ownership of `fd` and closes it itself.
|
||||
//
|
||||
// This is a first-class member of the interface, not a later phase: the
|
||||
// earlier branch deferred it and hardcoded `out->fd = -1` in its offer
|
||||
// poll, so its data plane could not move a single byte on the only
|
||||
// platform that matters.
|
||||
virtual MobileGLResult ShareFd(int fd, MobileGLByteSpan sideband) = 0;
|
||||
|
||||
// Receives one fd previously shared by the peer. On success *outFd owns
|
||||
// a descriptor this process must close. `sideband` receives the bytes
|
||||
// that travelled with it (may be empty) and must be at least
|
||||
// FdPassing::kMaxSidebandBytes: an fd offer is one datagram and cannot
|
||||
// be half-consumed, so the capacity is checked BEFORE anything is read
|
||||
// and a short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the
|
||||
// required size, having consumed nothing and dropped no descriptor.
|
||||
virtual MobileGLResult ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
|
||||
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) = 0;
|
||||
|
||||
// ---- lifecycle ------------------------------------------------------
|
||||
|
||||
// Idempotent. Tears down the WHOLE connection, not just this end:
|
||||
// both directions are half-closed, so after either endpoint calls it
|
||||
// neither side can send any more (SendFrame returns
|
||||
// MOBILEGL_ERR_TRANSPORT_CLOSED) and every waiter on either side is
|
||||
// unblocked. That is what closing a socket does, and the spawn
|
||||
// transport behaves the same way, so a one-sided contract here would
|
||||
// be a promise only the in-process implementation could keep.
|
||||
//
|
||||
// Messages already queued stay readable until drained: a peer that
|
||||
// shuts down right after sending does not lose its last message.
|
||||
virtual void Shutdown() = 0;
|
||||
|
||||
virtual TransportRole Role() const = 0;
|
||||
|
||||
protected:
|
||||
ITransport() = default;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -0,0 +1,293 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/InProcessTransport.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 "InProcessTransport.h"
|
||||
|
||||
#include "FdPassing.h"
|
||||
#include "Framing.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
namespace {
|
||||
struct FdOffer {
|
||||
int fd = -1;
|
||||
std::vector<std::uint8_t> sideband;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// One direction of the channel: everything queued FOR one endpoint.
|
||||
class InProcessChannel {
|
||||
public:
|
||||
struct Direction {
|
||||
std::mutex mutex;
|
||||
// One variable per predicate. A single cv signalled with
|
||||
// notify_one would let a SendFrame's wakeup land on a thread
|
||||
// blocked in ReceiveFd, which re-tests its own predicate and goes
|
||||
// straight back to sleep - leaving a queued message undelivered
|
||||
// until some unrelated later event. ITransport narrows the
|
||||
// contract to one dedicated reader thread, but a comment is not a
|
||||
// reason to ship a primitive that breaks the moment someone
|
||||
// splits the reader.
|
||||
std::condition_variable cv; // messages
|
||||
std::condition_variable fdCv; // fdOffers
|
||||
std::deque<std::vector<std::uint8_t>> messages;
|
||||
std::deque<FdOffer> fdOffers;
|
||||
bool closed = false;
|
||||
};
|
||||
|
||||
~InProcessChannel() {
|
||||
for (Direction& dir : m_directions) {
|
||||
for (FdOffer& offer : dir.fdOffers) {
|
||||
#if !defined(_WIN32)
|
||||
if (offer.fd >= 0) {
|
||||
::close(offer.fd);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
dir.fdOffers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Direction& Inbox(int endpoint) { return m_directions[endpoint]; }
|
||||
Direction& Outbox(int endpoint) { return m_directions[1 - endpoint]; }
|
||||
CondVarDoorbell& Bell(int endpoint) { return m_bells[endpoint]; }
|
||||
|
||||
void Close() {
|
||||
for (Direction& dir : m_directions) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(dir.mutex);
|
||||
dir.closed = true;
|
||||
}
|
||||
dir.cv.notify_all();
|
||||
dir.fdCv.notify_all();
|
||||
}
|
||||
// Anything parked on a ring doorbell has to come back too, or a
|
||||
// shutdown mid-frame hangs the peer forever. Kill, not Notify: a
|
||||
// ring is consumed by one Park, after which Doorbell::Wait re-tests
|
||||
// a condition nothing published and - the bell still reporting
|
||||
// alive - parks again, with no deadline forever. Only Dead() ends
|
||||
// that loop.
|
||||
for (CondVarDoorbell& bell : m_bells) {
|
||||
bell.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Direction m_directions[2];
|
||||
CondVarDoorbell m_bells[2];
|
||||
};
|
||||
|
||||
InProcessTransport::InProcessTransport(std::shared_ptr<InProcessChannel> channel, int endpoint)
|
||||
: m_channel(std::move(channel)), m_endpoint(endpoint) {}
|
||||
|
||||
InProcessTransport::~InProcessTransport() = default;
|
||||
|
||||
void InProcessTransport::CreatePair(std::unique_ptr<InProcessTransport>& outClient,
|
||||
std::unique_ptr<InProcessTransport>& outServer) {
|
||||
auto channel = std::make_shared<InProcessChannel>();
|
||||
outClient.reset(new InProcessTransport(channel, 0));
|
||||
outServer.reset(new InProcessTransport(channel, 1));
|
||||
}
|
||||
|
||||
MobileGLResult InProcessTransport::SendFrame(MobileGLByteSpan bytes) {
|
||||
if (bytes.size != 0 && bytes.data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
// Same cap as the byte-stream transports, so nothing legal here becomes
|
||||
// illegal the day the delivery mode changes to `spawn`.
|
||||
if (bytes.size > kMaxFramePayloadSize) {
|
||||
MGLOG_E("MG_Remote inproc: refusing a %llu byte message (cap %llu)",
|
||||
static_cast<unsigned long long>(bytes.size),
|
||||
static_cast<unsigned long long>(kMaxFramePayloadSize));
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(dir.mutex);
|
||||
if (dir.closed) {
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
const auto* first = static_cast<const std::uint8_t*>(bytes.data);
|
||||
dir.messages.emplace_back(first, first + bytes.size);
|
||||
}
|
||||
dir.cv.notify_one();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MobileGLResult InProcessTransport::ReceiveFrame(MobileGLMutableByteSpan buffer,
|
||||
std::uint64_t* outSize,
|
||||
std::uint32_t timeoutMs) {
|
||||
if (outSize != nullptr) {
|
||||
*outSize = 0;
|
||||
}
|
||||
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
|
||||
std::unique_lock<std::mutex> lock(dir.mutex);
|
||||
if (dir.messages.empty() && !dir.closed && timeoutMs != 0) {
|
||||
const auto ready = [&dir] { return !dir.messages.empty() || dir.closed; };
|
||||
if (timeoutMs == kWaitForever) {
|
||||
dir.cv.wait(lock, ready);
|
||||
} else {
|
||||
dir.cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
|
||||
}
|
||||
}
|
||||
if (dir.messages.empty()) {
|
||||
// Queued messages outlive the peer's Shutdown; only an empty inbox
|
||||
// is a closed one.
|
||||
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t>& front = dir.messages.front();
|
||||
const std::uint64_t size = front.size();
|
||||
if (outSize != nullptr) {
|
||||
*outSize = size;
|
||||
}
|
||||
if (buffer.size < size) {
|
||||
// Contract: the message STAYS QUEUED. The earlier branch's
|
||||
// transport failed the call and popped the message anyway, which
|
||||
// wedges the stream permanently the first time a reader guesses the
|
||||
// size wrong.
|
||||
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
if (size != 0) {
|
||||
if (buffer.data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer.data, front.data(), static_cast<std::size_t>(size));
|
||||
}
|
||||
dir.messages.pop_front();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
std::uint64_t InProcessTransport::PeekFrameSize() {
|
||||
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
|
||||
std::lock_guard<std::mutex> lock(dir.mutex);
|
||||
return dir.messages.empty() ? 0 : dir.messages.front().size();
|
||||
}
|
||||
|
||||
MobileGLResult InProcessTransport::ShareFd(int fd, MobileGLByteSpan sideband) {
|
||||
#if defined(_WIN32)
|
||||
(void)fd;
|
||||
(void)sideband;
|
||||
return MOBILEGL_ERR_UNSUPPORTED;
|
||||
#else
|
||||
if (fd < 0) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (sideband.size > FdPassing::kMaxSidebandBytes ||
|
||||
(sideband.size != 0 && sideband.data == nullptr)) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
// Same ownership rule as SCM_RIGHTS: the peer gets its own descriptor
|
||||
// for the same open file description and the caller keeps its own.
|
||||
const int duplicate = ::dup(fd);
|
||||
if (duplicate < 0) {
|
||||
MGLOG_E("MG_Remote inproc: dup failed (errno=%d)", errno);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
|
||||
FdOffer offer;
|
||||
offer.fd = duplicate;
|
||||
if (sideband.size != 0) {
|
||||
const auto* first = static_cast<const std::uint8_t*>(sideband.data);
|
||||
offer.sideband.assign(first, first + sideband.size);
|
||||
}
|
||||
|
||||
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(dir.mutex);
|
||||
if (dir.closed) {
|
||||
::close(duplicate);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
dir.fdOffers.push_back(std::move(offer));
|
||||
}
|
||||
dir.fdCv.notify_one();
|
||||
return MOBILEGL_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
MobileGLResult InProcessTransport::ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
|
||||
std::uint64_t* outSidebandSize,
|
||||
std::uint32_t timeoutMs) {
|
||||
if (outFd == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
*outFd = -1;
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = 0;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
(void)sideband;
|
||||
(void)timeoutMs;
|
||||
return MOBILEGL_ERR_UNSUPPORTED;
|
||||
#else
|
||||
// Symmetric with FdPassing::ReceiveFd so callers behave identically in
|
||||
// both delivery modes.
|
||||
if (sideband.size < FdPassing::kMaxSidebandBytes) {
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = FdPassing::kMaxSidebandBytes;
|
||||
}
|
||||
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
if (sideband.data == nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
|
||||
std::unique_lock<std::mutex> lock(dir.mutex);
|
||||
if (dir.fdOffers.empty() && !dir.closed && timeoutMs != 0) {
|
||||
const auto ready = [&dir] { return !dir.fdOffers.empty() || dir.closed; };
|
||||
if (timeoutMs == kWaitForever) {
|
||||
dir.fdCv.wait(lock, ready);
|
||||
} else {
|
||||
dir.fdCv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
|
||||
}
|
||||
}
|
||||
if (dir.fdOffers.empty()) {
|
||||
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
FdOffer offer = std::move(dir.fdOffers.front());
|
||||
dir.fdOffers.pop_front();
|
||||
if (!offer.sideband.empty()) {
|
||||
std::memcpy(sideband.data, offer.sideband.data(), offer.sideband.size());
|
||||
}
|
||||
if (outSidebandSize != nullptr) {
|
||||
*outSidebandSize = offer.sideband.size();
|
||||
}
|
||||
*outFd = offer.fd;
|
||||
return MOBILEGL_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Whole-connection teardown, as ITransport::Shutdown documents: both
|
||||
// directions are half-closed and both ring doorbells are KILLED, because a
|
||||
// peer parked on a ring doorbell mid-frame would otherwise never come back
|
||||
// (a mere ring is consumed once and the waiter parks again).
|
||||
void InProcessTransport::Shutdown() { m_channel->Close(); }
|
||||
|
||||
Doorbell& InProcessTransport::PeerDoorbell() { return m_channel->Bell(1 - m_endpoint); }
|
||||
|
||||
Doorbell& InProcessTransport::SelfDoorbell() { return m_channel->Bell(m_endpoint); }
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user