mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
41
Commits
6672778b80
...
e7a6a72f6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+684
-4
@@ -7,6 +7,14 @@ on:
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
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:
|
||||
@@ -295,6 +303,343 @@ 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
|
||||
@@ -322,6 +667,35 @@ jobs:
|
||||
- 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
|
||||
@@ -533,6 +907,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
|
||||
@@ -542,6 +917,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 }})
|
||||
@@ -755,10 +1133,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
|
||||
@@ -767,14 +1430,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
|
||||
@@ -826,6 +1494,17 @@ jobs:
|
||||
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
|
||||
@@ -843,7 +1522,8 @@ jobs:
|
||||
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 P1, when the mapping file exists to diff against.
|
||||
# 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
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
|
||||
# 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")
|
||||
|
||||
@@ -451,6 +456,22 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
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
|
||||
@@ -524,6 +545,13 @@ 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
|
||||
|
||||
@@ -330,6 +330,26 @@ namespace MobileGL::MG_Config {
|
||||
// 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;
|
||||
|
||||
@@ -244,6 +244,13 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
@@ -140,14 +141,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// addresses again - the cached pointers cannot go stale. Invalidation is
|
||||
// exactly the pointer compare below.
|
||||
using FbBindingSlot =
|
||||
std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
|
||||
static const MG_State::GLState::GLContext* g_fbSlotCacheContext = nullptr;
|
||||
std::remove_reference_t<decltype(MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
|
||||
static const void* g_fbSlotCacheContext = nullptr;
|
||||
static Array<FbBindingSlot*, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fbSlotCache = {};
|
||||
static inline FbBindingSlot& GetFramebufferBindingSlotFast(FramebufferTarget target) {
|
||||
MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get();
|
||||
const void* ctx = MGB_CTX_IDENTITY;
|
||||
if (ctx != g_fbSlotCacheContext) {
|
||||
auto& live = *MGB_CTX;
|
||||
for (SizeT i = 0; i < g_fbSlotCache.size(); ++i) {
|
||||
g_fbSlotCache[i] = &ctx->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
|
||||
g_fbSlotCache[i] = &live.GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
|
||||
}
|
||||
g_fbSlotCacheContext = ctx;
|
||||
}
|
||||
@@ -258,7 +260,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -354,7 +356,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
// Only sync up to the high-water mark of app-touched points; the fixed array is 84
|
||||
// deep but apps bind a handful, so the never-touched tail is already at GL default 0.
|
||||
auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);
|
||||
auto bindingPointCnt = MGB_CTX->GetTouchedBufferBindingPointCount(target);
|
||||
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
|
||||
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
|
||||
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
|
||||
@@ -367,7 +369,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
|
||||
}
|
||||
for (SizeT i = 0; i < bindingPointCnt; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(target, i);
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), 0);
|
||||
@@ -426,7 +428,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT pointCount = std::min<SizeT>(
|
||||
bufferCount, MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
|
||||
for (SizeT i = 0; i < pointCount; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
|
||||
const auto& obj = point.GetBoundObject();
|
||||
// A stride-0 slot (two consecutive gl_NextBuffer entries) captures nothing and
|
||||
// needs no binding; anything else with no buffer never got past the frontend.
|
||||
@@ -459,10 +461,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// pull the real contents back (BufferObject::SyncGpuWrites).
|
||||
void MarkShaderStorageBuffersGpuWritten() {
|
||||
const SizeT bindingPointCnt =
|
||||
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
MGB_CTX->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
for (SizeT i = 0; i < bindingPointCnt; ++i) {
|
||||
const auto& obj =
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
|
||||
if (obj) obj->MarkGpuWritten();
|
||||
}
|
||||
}
|
||||
@@ -471,14 +473,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
|
||||
const SizeT pointCount = MGB_CTX->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
|
||||
for (const Int glBinding : glBindings) {
|
||||
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
|
||||
const Int esslBinding = esslBindingTop - glBinding;
|
||||
// Already diagnosed once when the block was transpiled; nothing was bound to it
|
||||
// there either, so there is nothing to unbind here.
|
||||
if (esslBinding < 0) continue;
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::AtomicCounter,
|
||||
static_cast<Uint>(glBinding));
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
@@ -515,7 +517,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(target).GetBoundObject();
|
||||
auto& bufferObject = MGB_CTX->GetBufferBindingSlot(target).GetBoundObject();
|
||||
if (!bufferObject) {
|
||||
g_GLESFuncs.glBindBuffer(glTarget, 0);
|
||||
return;
|
||||
@@ -659,7 +661,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// context since indirect draws now execute natively on the GPU.
|
||||
if (includeIndirectBuffer) {
|
||||
auto& possibleIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (possibleIndirectBuffer) {
|
||||
SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER);
|
||||
}
|
||||
@@ -889,7 +891,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const SizeT packedStride = program->GetTransformFeedbackPackedStride();
|
||||
const SizeT modelledVertices =
|
||||
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());
|
||||
static_cast<SizeT>(MGB_CTX->GetTransformFeedbackCapturedVertices());
|
||||
const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
|
||||
if (packedStride == 0 || vertices == 0) {
|
||||
// The scatter path redirected the DRIVER's capture into the scratch buffer,
|
||||
@@ -984,7 +986,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// not captured, and opening the span would also subject it to the capture
|
||||
// primitive-mode rule the paused draw is exempt from.
|
||||
if (!xfb.pending || xfb.paused) return;
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
|
||||
if (!program) {
|
||||
// The pending flag is deliberately NOT consumed here. It used to be cleared
|
||||
// before this check, so a single draw that could not see the capture program
|
||||
@@ -1004,7 +1006,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// recording it here keeps End independent of the frontend capture state.
|
||||
const SizeT bufferCount = program->GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
const auto& bufferObject = point.GetBoundObject();
|
||||
if (!bufferObject) continue;
|
||||
@@ -1241,7 +1243,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
if (!program) return;
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao || !vaoTwin) return;
|
||||
|
||||
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
||||
@@ -1272,7 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (Uint32 remaining = memo.pendingMask; remaining != 0; remaining &= remaining - 1) {
|
||||
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
||||
|
||||
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
||||
const auto& currentValue = MGB_CTX->GetCurrentVertexAttribute(location);
|
||||
const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location));
|
||||
switch (typeInfo.baseType) {
|
||||
case MG_State::GLState::VertexAttribBaseType::Float:
|
||||
@@ -1370,7 +1372,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static void CaptureUnitBindings(Int maxTouchedUnit, Vector<UnitBindingsSnapshot>& out) {
|
||||
out.resize(static_cast<SizeT>(maxTouchedUnit + 1));
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
auto& snapshot = out[static_cast<SizeT>(unit)];
|
||||
const auto& slots = textureUnit.GetAllBindingSlots();
|
||||
for (SizeT i = 0; i < slots.size(); ++i) {
|
||||
@@ -1383,7 +1385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool UnitBindingsUnchanged(Int maxTouchedUnit, const Vector<UnitBindingsSnapshot>& snapshots) {
|
||||
if (snapshots.size() != static_cast<SizeT>(maxTouchedUnit + 1)) return false;
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const auto& snapshot = snapshots[static_cast<SizeT>(unit)];
|
||||
const auto& slots = textureUnit.GetAllBindingSlots();
|
||||
for (SizeT i = 0; i < slots.size(); ++i) {
|
||||
@@ -1411,8 +1413,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// bump - so epoch equality alone proves the bindings a consumer resolved against
|
||||
// are the bindings on the units now.
|
||||
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
|
||||
const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Uint64 contextId = MGB_CTX->GetTextureContextId();
|
||||
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// Two accessor calls whichever way the shutter goes; only the unit WALK is
|
||||
// gated, and that walk reads no GLContext accessor of its own.
|
||||
@@ -1512,10 +1514,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
DrawTextureSyncKeys CaptureDrawTextureSyncKeys() {
|
||||
DrawTextureSyncKeys keys;
|
||||
keys.contextId = MG_State::pGLContext->GetTextureContextId();
|
||||
keys.contextId = MGB_CTX->GetTextureContextId();
|
||||
// Units past the frontend's high-water mark have provably-empty slots.
|
||||
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
|
||||
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
keys.maxTouchedUnit = MGB_CTX->GetMaxTouchedTextureUnit();
|
||||
keys.samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The three reads above; CurrentUnitBindingsEpoch counts its own two.
|
||||
@@ -1577,7 +1579,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_unitTextureSyncListValid = false;
|
||||
g_unitTextureSyncList.clear();
|
||||
for (Int index = 0; index <= maxTouchedUnit; ++index) {
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);
|
||||
auto& unit = MGB_CTX->GetTextureUnitObject(index);
|
||||
for (const auto& bindingSlot : unit.GetAllBindingSlots()) {
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
// An image-less default texture (name 0) is the slot's initial / "unbound"
|
||||
@@ -1725,7 +1727,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
||||
auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
|
||||
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
|
||||
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
|
||||
g_imageUnitHighWaterMark = unit + 1;
|
||||
@@ -1817,7 +1819,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_writableImageBufferUnitCount == 0) return;
|
||||
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
|
||||
if (!g_writableImageBufferUnits[unit]) continue;
|
||||
const auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
||||
const auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
|
||||
if (!IsWritableImageBufferTexture(imageBinding)) {
|
||||
TrackWritableImageBufferUnit(unit, false);
|
||||
continue;
|
||||
@@ -2023,7 +2025,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
Uint16 currentRenderStateVersion = MGB_CTX->GetRenderStateParametersVersion();
|
||||
const Bool forceFullPush = g_forceFullRenderStateResync;
|
||||
g_forceFullRenderStateResync = false;
|
||||
// The alpha discipline for widened colour attachments (see the header comment on
|
||||
@@ -2051,7 +2053,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
|
||||
}
|
||||
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const auto& parameters = MGB_CTX->GetRenderStateParameters();
|
||||
|
||||
// The frontend has ONE version for the whole parameter block, so a per-draw blend
|
||||
// toggle used to re-diff all ~40 pieces of state field by field on every draw
|
||||
@@ -2080,7 +2082,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
||||
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
||||
|
||||
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
||||
IntVec4 backendViewport = MGB_CTX->GetViewport();
|
||||
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -2163,7 +2165,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// never turns it on, so the driver has to be told to write raw. Without this a render
|
||||
// into an sRGB colour buffer comes back encoded once too often (the shader's own
|
||||
// decode on the next fetch then leaves the value one conversion short).
|
||||
const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
||||
const Bool srgbWrites = MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
||||
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
|
||||
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
|
||||
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB)
|
||||
@@ -2839,11 +2841,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// member set it has to be told.
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
(twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
|
||||
static_cast<Int>(MGB_CTX->GetPatchVertices()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
|
||||
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
|
||||
MGB_CTX->GetPatchDefaultOuterLevel()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
|
||||
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
|
||||
MGB_CTX->GetPatchDefaultInnerLevel())))) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
@@ -2954,7 +2956,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// resolved-buffers memo on the twin), the VAO sync and the draw-time bind
|
||||
// below. Nothing in between can invalidate it — the bound VAO is pinned by
|
||||
// the context, and no step here erases or replaces a live VAO's twin.
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
|
||||
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
|
||||
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
|
||||
// Early config-version read: see the note on SyncNeccessaryBuffers - issuing
|
||||
@@ -2965,7 +2967,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// either, and none can run inside this preparation. GetProgramForDraw is a
|
||||
// cross-TU call with a guarded static inside - repeating it per stage showed
|
||||
// up in draw-loop profiles.
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// THE per-draw denominator for Espryt, plus this function's own two accessor
|
||||
// calls (the VAO and the draw program). Everything the callees below read is
|
||||
@@ -3045,7 +3047,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
|
||||
Array<TextureTarget, (SizeT)TextureTarget::TextureTargetCount> claimedByFrontendTarget{};
|
||||
claimedByFrontendTarget.fill(TextureTarget::Unknown);
|
||||
@@ -3209,7 +3211,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
g_unitSamplerWalkValid = false;
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto& samplerObject = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
if (samplerObject) {
|
||||
if (auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject)) {
|
||||
backendSampler->Bind(unit);
|
||||
@@ -3359,7 +3361,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void BindCurrentTextures() {
|
||||
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());
|
||||
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MGB_CTX->GetProgramForDraw());
|
||||
}
|
||||
|
||||
// Binds the current program's backend object and re-establishes its per-program
|
||||
@@ -3479,7 +3481,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Connect buffer to backend binding point
|
||||
auto binding = currentProgram->GetUniformBlockBinding(i);
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, binding);
|
||||
auto& bufferObj = point.GetBoundObject();
|
||||
auto range = point.GetRange();
|
||||
|
||||
@@ -3577,7 +3579,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
samplerBinding.lastAssignedUnit = unit;
|
||||
}
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
auto& samplerObject = textureUnit.GetSamplerObject();
|
||||
const auto& texture2D =
|
||||
textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
@@ -3659,7 +3661,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PrepareForCompute, where the current program (and therefore its registry twin)
|
||||
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
||||
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -3709,7 +3711,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
|
||||
// so an unanswerable program counts as needing them.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
if (!currentProgram || program == nullptr ||
|
||||
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
||||
@@ -3818,12 +3820,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// times; rasterizer discard means there are no fragments to gate at all, so replaying
|
||||
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
|
||||
// open gate, i.e. to the pre-emulation behaviour, rather than to wrong data.
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
if (MGB_CTX->IsTransformFeedbackActive() ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const auto& parameters = MGB_CTX->GetRenderStateParameters();
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
||||
@@ -4059,7 +4061,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a
|
||||
// pipeline bound this is its compute stage program, which is a whole program on its
|
||||
// own - the graphics composite a draw builds carries no compute stage.
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDispatch();
|
||||
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
||||
|
||||
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
|
||||
@@ -4085,12 +4087,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
GLuint GetBackendProgramId(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
if (!MGB_CTX->ValidateProgramName(program)) {
|
||||
MGLOG_E_ONCE("Invalid frontend program object: %u", program);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
auto& programObject = MGB_CTX->GetProgramObject(program);
|
||||
if (!programObject) {
|
||||
MGLOG_E_ONCE("Program object %u is null.", program);
|
||||
return 0;
|
||||
@@ -4153,7 +4155,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// color must go through glClearBufferfv, which GLES does not clamp.
|
||||
GLbitfield remainingMask = mask;
|
||||
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;
|
||||
const FloatVec4& cc = MGB_CTX->GetRenderStateParameters().ClearColor;
|
||||
const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f ||
|
||||
cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f;
|
||||
// A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE
|
||||
@@ -4212,7 +4214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb);
|
||||
const GLenum rbErr = g_GLESFuncs.glGetError();
|
||||
const auto& feFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
int feDb0 = -1, feDb1 = -1;
|
||||
Uint feIdx = 0, feVer = 0;
|
||||
if (feFbo) {
|
||||
@@ -4377,7 +4379,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
|
||||
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();
|
||||
}
|
||||
@@ -4393,13 +4395,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace
|
||||
|
||||
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
||||
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
||||
return RestartSubstitutionKind::None;
|
||||
}
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
||||
if (fixedMax == 0) return RestartSubstitutionKind::None;
|
||||
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
const Uint32 restartIndex = MGB_CTX->GetPrimitiveRestartIndex();
|
||||
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
|
||||
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
|
||||
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
|
||||
@@ -4439,7 +4441,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
||||
const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
const Uint32 applicationRestartIndex = MGB_CTX->GetPrimitiveRestartIndex();
|
||||
const auto& indexBuffer = BoundElementArrayBuffer();
|
||||
|
||||
const Uint8* source = nullptr;
|
||||
@@ -4567,7 +4569,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
DrawSyncFlags syncBit = DrawSyncBit::None;
|
||||
PrepareForDraw(syncBit);
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
|
||||
if (currentVAO) {
|
||||
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
|
||||
if (backendVAOSlot && *backendVAOSlot) {
|
||||
@@ -4606,7 +4608,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ladder and the indirect executors do. Without it every sub-draw of a
|
||||
// glMultiDrawArrays read draw index 0.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
|
||||
if (currentVAO) {
|
||||
@@ -4677,7 +4679,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
||||
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
|
||||
}
|
||||
@@ -4708,8 +4710,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!drawBuffer) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
return;
|
||||
@@ -4779,7 +4781,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
|
||||
drawcount, stride, "MultiDrawArraysIndirect");
|
||||
}
|
||||
@@ -4810,8 +4812,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!drawBuffer) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
return;
|
||||
@@ -4970,7 +4972,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
||||
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
|
||||
"DrawElementsIndirect");
|
||||
@@ -5011,7 +5013,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const auto& drawIndirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
|
||||
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
||||
}
|
||||
@@ -5256,8 +5258,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// restore. Drop the flag so it is not misattributed to the emulation's own work.
|
||||
DrainBlitErrors();
|
||||
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
|
||||
if (MGB_CTX->IsTransformFeedbackActive() &&
|
||||
!MGB_CTX->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
|
||||
g_GLESFuncs.glPauseTransformFeedback();
|
||||
m_pausedTransformFeedback = true;
|
||||
DrainBlitErrors();
|
||||
@@ -6041,8 +6043,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A no-op on every driver that honours a non-zero destination array layer, which is all
|
||||
// of them but the probed one. Whatever it performs itself is taken out of the mask.
|
||||
mask &= ~BlitLayeredDestinationAspects(
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
|
||||
srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask);
|
||||
if (mask != 0) {
|
||||
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
@@ -6104,8 +6106,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
auto unit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto unit = MGB_CTX->GetActiveTextureUnit();
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
|
||||
auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) {
|
||||
@@ -6182,7 +6184,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// The frontend's current PACK parameters, for readbacks the ES driver serves
|
||||
// directly with the client's layout.
|
||||
static PixelStoreImpl::PackState PackStateFromContext() {
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
return {static_cast<GLint>(packParams.Alignment), static_cast<GLint>(packParams.RowLength),
|
||||
static_cast<GLint>(packParams.SkipRows), static_cast<GLint>(packParams.SkipPixels)};
|
||||
}
|
||||
@@ -6372,7 +6374,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Util::ConvertGLEnumToString(err).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(target).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(format).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
MGB_CTX->RecordError(
|
||||
ConvertGLESErrorToErrorCode(err),
|
||||
MakeUnique<GenericErrorInfo>("DirectGLES", operation,
|
||||
MG_Util::ConvertGLEnumToString(err)));
|
||||
@@ -6696,8 +6698,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Bind necessary FBO and texture
|
||||
BindCurrentFBO(FramebufferTarget::Read);
|
||||
Uint activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit)
|
||||
Uint activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
|
||||
const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit)
|
||||
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
||||
.GetBoundObject();
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
@@ -6791,8 +6793,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Bind necessary FBO and texture
|
||||
BindCurrentFBO(FramebufferTarget::Read);
|
||||
auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
|
||||
auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
|
||||
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
|
||||
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
||||
.GetBoundObject();
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
@@ -6929,8 +6931,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
||||
DebugImpl::OpenGLScopeMarker marker(__func__);
|
||||
#endif
|
||||
auto unitIndex = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(unitIndex);
|
||||
auto unitIndex = MGB_CTX->GetActiveTextureUnit();
|
||||
auto& unit = MGB_CTX->GetTextureUnitObject(unitIndex);
|
||||
auto& slot = unit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target));
|
||||
auto& texture = slot.GetBoundObject();
|
||||
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
|
||||
@@ -7354,8 +7356,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// effect by the block's next use.
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
if (!storageBlockName) return;
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) return;
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
if (!MGB_CTX->ValidateProgramName(program)) return;
|
||||
auto& programObject = MGB_CTX->GetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
|
||||
@@ -7551,7 +7553,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
template <typename FillRow>
|
||||
static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels,
|
||||
const char* what, FillRow&& fillRow) {
|
||||
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 = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
@@ -7559,7 +7561,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT rowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride + rowBytes;
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
|
||||
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what);
|
||||
@@ -8541,7 +8543,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
if (!pixelPackBufferObject && pixels == nullptr) {
|
||||
return true;
|
||||
}
|
||||
@@ -8771,7 +8773,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
if (!pixelPackBufferObject && pixels == nullptr) {
|
||||
return true;
|
||||
}
|
||||
@@ -9038,7 +9040,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type
|
||||
// the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so
|
||||
// it always takes the conversion path (which swaps on the CPU).
|
||||
const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;
|
||||
const Bool packSwapBytes = MGB_CTX->GetPixelStoreParameters(false).SwapBytes;
|
||||
// The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly
|
||||
// the right thing to ask here.
|
||||
const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment();
|
||||
@@ -9081,7 +9083,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (the driver-level binding used to stay on the user PBO after this call,
|
||||
// capturing subsequent client-memory readbacks into it).
|
||||
auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
Bool usePBO = false;
|
||||
GLuint packBufferId = 0;
|
||||
if (pixelPackBufferObject) {
|
||||
@@ -9191,10 +9193,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("GetTexImage: SyncCurrentFBO()");
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
|
||||
auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
|
||||
auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
|
||||
MGLOG_D("GetTexImage: active texture unit = %u", activeTextureUnit);
|
||||
|
||||
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
|
||||
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
|
||||
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
||||
.GetBoundObject();
|
||||
|
||||
@@ -9417,7 +9419,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Each slice is packed as its own 2D image, so the per-slice call must not apply
|
||||
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
|
||||
// over them, using the same layout StoreWideRowsToClient computes.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(conversionMapping, type);
|
||||
const SizeT rowPixels =
|
||||
static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : size.x());
|
||||
@@ -9507,7 +9509,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Handle PBO. The pack binding is scoped: it returns to the resting 0 state
|
||||
// on every exit path, so a later readback can never land in a stale PBO.
|
||||
auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
Bool usePBO = false;
|
||||
GLuint packBufferId = 0;
|
||||
if (pixelPackBufferObject) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Managers.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
@@ -3641,9 +3642,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// that needs no work costs the same nothing per draw that any other synced texture does.
|
||||
void BackendTextureObject::StampViewSyncKeys(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (MG_State::pGLContext) {
|
||||
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
if (MGB_CTX_LIVE) {
|
||||
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
||||
}
|
||||
m_syncedContentVersion = stateTextureObject->GetContentVersion();
|
||||
@@ -3770,9 +3771,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// version - and backend-side storage resets clear m_isInitialized. Restricted to
|
||||
// Mipmap storage like the probe fast path: a buffer texture's backing store can move
|
||||
// without any of these keys noticing.
|
||||
if (m_isInitialized && m_syncedShapeContextId != 0 && MG_State::pGLContext &&
|
||||
m_syncedShapeContextId == MG_State::pGLContext->GetTextureContextId() &&
|
||||
m_syncedShapeGeneration == MG_State::pGLContext->GetSamplingResolutionGeneration() &&
|
||||
if (m_isInitialized && m_syncedShapeContextId != 0 && MGB_CTX_LIVE &&
|
||||
m_syncedShapeContextId == MGB_CTX->GetTextureContextId() &&
|
||||
m_syncedShapeGeneration == MGB_CTX->GetSamplingResolutionGeneration() &&
|
||||
m_syncedContentVersion == stateTextureObject->GetContentVersion() &&
|
||||
m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() &&
|
||||
stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
|
||||
@@ -3841,9 +3842,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// The probe just proved "fully synced" from the real state, so the cheap
|
||||
// gate may be (re)stamped here: the coarse generation only ever goes stale
|
||||
// from OTHER textures' churn, and this draw re-validated this one.
|
||||
if (MG_State::pGLContext) {
|
||||
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
if (MGB_CTX_LIVE) {
|
||||
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
||||
}
|
||||
return;
|
||||
@@ -4732,9 +4733,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Same instant, so the cheap gate's keys describe exactly this synced state.
|
||||
// Only Mipmap storage may arm it - the gate refuses other storage types anyway,
|
||||
// but a stale trio must not linger on an object that later switches type.
|
||||
if (MG_State::pGLContext && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
|
||||
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
if (MGB_CTX_LIVE && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
|
||||
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
|
||||
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
||||
} else {
|
||||
m_syncedShapeContextId = 0;
|
||||
@@ -5352,7 +5353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// read buffer names no colour attachment at all.
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() {
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -5457,7 +5458,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Bool IsFixedPointFallbackReadAttachment() {
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
return false;
|
||||
}
|
||||
@@ -6227,7 +6228,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// outside the frontend's array, which cannot be addressed at all.
|
||||
Uint BoundImageUnitFormat(Int unit) {
|
||||
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
|
||||
return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);
|
||||
return static_cast<Uint>(MGB_CTX->GetImageTextureBinding(unit).Format);
|
||||
}
|
||||
|
||||
// Combines one (unit, format) pair into a running digest. Commutative, so the order
|
||||
@@ -7189,19 +7190,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// patch size - so a program built for one value is stale for another. Recorded here
|
||||
// and compared on the draw path (SyncCurrentProgram), the same shape as the
|
||||
// storage-block and image-format signatures next to it.
|
||||
const Uint patchVertices = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchVertices()
|
||||
const Uint patchVertices = MGB_CTX_LIVE
|
||||
? MGB_CTX->GetPatchVertices()
|
||||
: 3u;
|
||||
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
|
||||
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
|
||||
// into the same stage (ES has no such state and no entry point to forward them to), so
|
||||
// they are recorded and compared alongside the patch size - the two move together, as
|
||||
// BuildPassthroughTessControlEssl's contract says.
|
||||
m_passthroughTessControlOuterLevel = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchDefaultOuterLevel()
|
||||
m_passthroughTessControlOuterLevel = MGB_CTX_LIVE
|
||||
? MGB_CTX->GetPatchDefaultOuterLevel()
|
||||
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
m_passthroughTessControlInnerLevel = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchDefaultInnerLevel()
|
||||
m_passthroughTessControlInnerLevel = MGB_CTX_LIVE
|
||||
? MGB_CTX->GetPatchDefaultInnerLevel()
|
||||
: FloatVec2(1.0f, 1.0f);
|
||||
|
||||
if (tessEvalShaderIndex < 0 ||
|
||||
@@ -8747,8 +8748,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
|
||||
stateRBOObject->GetExternalIndex(), width, height, samples,
|
||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->RecordError(
|
||||
ErrorCode::OutOfMemory,
|
||||
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
|
||||
"The ES driver could not allocate the renderbuffer storage."));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#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>
|
||||
@@ -42,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
|
||||
@@ -84,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;
|
||||
@@ -92,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();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#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>
|
||||
@@ -2294,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 =
|
||||
|
||||
@@ -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());
|
||||
@@ -783,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();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#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"
|
||||
@@ -267,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);
|
||||
@@ -334,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;
|
||||
@@ -399,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;
|
||||
@@ -442,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;
|
||||
@@ -462,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;
|
||||
@@ -493,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;
|
||||
@@ -520,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) {
|
||||
@@ -530,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;
|
||||
@@ -564,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;
|
||||
@@ -579,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;
|
||||
@@ -613,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,
|
||||
@@ -628,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);
|
||||
}
|
||||
|
||||
@@ -713,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;
|
||||
@@ -738,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);
|
||||
}
|
||||
|
||||
@@ -783,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);
|
||||
@@ -814,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) {
|
||||
@@ -839,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;
|
||||
@@ -862,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;
|
||||
}
|
||||
@@ -907,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) {
|
||||
@@ -968,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)) {
|
||||
@@ -998,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);
|
||||
}
|
||||
|
||||
@@ -1233,8 +1234,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
|
||||
!query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) {
|
||||
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
|
||||
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
query->pausedPrimitiveSnapshot;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
@@ -1281,7 +1282,7 @@ 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();
|
||||
|
||||
@@ -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"
|
||||
@@ -503,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;
|
||||
@@ -552,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 &&
|
||||
@@ -779,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;
|
||||
@@ -809,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(),
|
||||
@@ -818,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
|
||||
@@ -834,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(),
|
||||
@@ -844,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).
|
||||
@@ -989,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(),
|
||||
@@ -1013,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
|
||||
@@ -1149,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);
|
||||
|
||||
@@ -1186,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
|
||||
@@ -1263,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);
|
||||
|
||||
@@ -1292,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
|
||||
@@ -1648,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(),
|
||||
@@ -1659,7 +1660,7 @@ 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();
|
||||
@@ -1803,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.
|
||||
@@ -1847,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
|
||||
@@ -1885,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;
|
||||
|
||||
@@ -1935,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 &&
|
||||
@@ -1965,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,
|
||||
@@ -2010,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'",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +11,7 @@
|
||||
#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"
|
||||
@@ -806,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 {
|
||||
@@ -949,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 =
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "VertexInputStateBuilder.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/SamplerState/SamplerObject.h"
|
||||
@@ -460,12 +461,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// round trip), which is why the honest-but-lossy path was kept over widening every
|
||||
// default-framebuffer Y-flip/pre-transform helper to floats. See the KNOWN INFIDELITY
|
||||
// note in MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp.
|
||||
const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
const FloatVec4& stored = MGB_CTX->GetViewportIndexed(index);
|
||||
const IntVec4 viewportState(static_cast<Int>(std::lround(stored.x())),
|
||||
static_cast<Int>(std::lround(stored.y())),
|
||||
static_cast<Int>(std::lround(stored.z())),
|
||||
static_cast<Int>(std::lround(stored.w())));
|
||||
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
const FloatVec2& depthRange = MGB_CTX->GetDepthRangeIndexed(index);
|
||||
const IntVec2 logicalExtent = isDefaultFramebuffer
|
||||
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
||||
: framebufferExtent;
|
||||
@@ -520,7 +521,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void ApplyBlendConstants(VkCommandBuffer commandBuffer) {
|
||||
const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();
|
||||
const FloatVec4& blendColor = MGB_CTX->GetBlendColor();
|
||||
const float blendConstants[4] = {
|
||||
blendColor.x(),
|
||||
blendColor.y(),
|
||||
@@ -553,8 +554,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void ApplyPolygonOffsetState(VkCommandBuffer commandBuffer) {
|
||||
const Float constantFactor = MG_State::pGLContext->GetPolygonOffsetUnits();
|
||||
const Float slopeFactor = MG_State::pGLContext->GetPolygonOffsetFactor();
|
||||
const Float constantFactor = MGB_CTX->GetPolygonOffsetUnits();
|
||||
const Float slopeFactor = MGB_CTX->GetPolygonOffsetFactor();
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
if (shadow.depthBiasValid && shadow.depthBiasConstantFactor == constantFactor &&
|
||||
shadow.depthBiasSlopeFactor == slopeFactor) {
|
||||
@@ -567,7 +568,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void ApplyLineWidthState(VkCommandBuffer commandBuffer) {
|
||||
Float lineWidth = MG_State::pGLContext->GetLineWidth();
|
||||
Float lineWidth = MGB_CTX->GetLineWidth();
|
||||
if (MG_Backend::pActiveBackendObject != nullptr) {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin;
|
||||
@@ -646,8 +647,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void ApplyStencilState(VkCommandBuffer commandBuffer) {
|
||||
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
|
||||
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
|
||||
const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front);
|
||||
const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back);
|
||||
const Uint32 frontReference = static_cast<Uint32>(std::max(frontStencil.Ref, 0));
|
||||
const Uint32 backReference = static_cast<Uint32>(std::max(backStencil.Ref, 0));
|
||||
|
||||
@@ -1240,11 +1241,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void RecordClearBufferError(const char* func, ErrorCode code, const char* message) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
|
||||
MGB_CTX->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
|
||||
}
|
||||
|
||||
static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
|
||||
MGB_CTX->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
|
||||
}
|
||||
|
||||
static Bool HasDistinctCompleteDepthStencilTextureAttachments(
|
||||
@@ -1300,7 +1301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static void RecordUnsupportedFramebufferError(const char* func) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
MGB_CTX->RecordError(
|
||||
ErrorCode::InvalidFramebufferOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"DirectVulkan", func,
|
||||
@@ -2768,7 +2769,7 @@ void main() {
|
||||
// glReadPixels final conversion: GL_CLAMP_READ_COLOR defaults to GL_FIXED_ONLY,
|
||||
// clamping fixed-point (normalized) buffers to [0,1] - visible for SNORM reads.
|
||||
if (applyReadColorClamp && wideType == GL_FLOAT) {
|
||||
const GLenum clampMode = MG_State::pGLContext->GetClampReadColor();
|
||||
const GLenum clampMode = MGB_CTX->GetClampReadColor();
|
||||
const Bool clamp = clampMode == GL_TRUE ||
|
||||
(clampMode == GL_FIXED_ONLY && !IsFloatingPointReadbackFormat(srcFormat));
|
||||
if (clamp) {
|
||||
@@ -2985,7 +2986,7 @@ void main() {
|
||||
inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) {
|
||||
ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap;
|
||||
const auto& currentDrawFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) {
|
||||
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
|
||||
// gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow
|
||||
@@ -3445,8 +3446,8 @@ void main() {
|
||||
// with restart off it is a legitimate index and excluding it would truncate the
|
||||
// converted stream by exactly that vertex.
|
||||
const Bool primitiveRestartActive =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
const Uint32 restartSentinel = indexSize == 1 ? 0xFFu : indexSize == 2 ? 0xFFFFu : 0xFFFFFFFFu;
|
||||
Uint32 maxIndex = 0;
|
||||
Bool sawIndex = false;
|
||||
@@ -3923,7 +3924,7 @@ void main() {
|
||||
}
|
||||
|
||||
const auto glType = programObj.vertexInputTypes[location];
|
||||
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
||||
const auto& currentValue = MGB_CTX->GetCurrentVertexAttribute(location);
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
const void* sourceData = nullptr;
|
||||
VkDeviceSize sourceSize = 0;
|
||||
@@ -4057,7 +4058,7 @@ void main() {
|
||||
Bool substituteRestart = false;
|
||||
// One bulk parameters fetch instead of up to three accessor calls per indexed
|
||||
// draw; all three inputs are pure reads of these fields.
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
const Uint32 restartIndex = rsp.PrimitiveRestartIndex;
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
|
||||
@@ -4812,9 +4813,9 @@ void main() {
|
||||
Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const {
|
||||
constexpr Uint32 kFullCoverage = 0xffffffffu;
|
||||
if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
|
||||
return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;
|
||||
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
|
||||
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
|
||||
return MGB_CTX->GetRenderStateParameters().SampleMaskValue;
|
||||
}
|
||||
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
@@ -4825,7 +4826,7 @@ void main() {
|
||||
// field (RenderState.cpp), so the hashed values are bit-identical. This runs on
|
||||
// every draw whose pipeline-state version moved (a per-draw GL_BLEND toggle),
|
||||
// where the accessor-call overhead dominated the hash itself.
|
||||
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters();
|
||||
Uint64 capabilityBits = 0;
|
||||
capabilityBits |= p.CullFaceEnabled ? 1ull << 0 : 0;
|
||||
capabilityBits |= p.DepthTestEnabled ? 1ull << 1 : 0;
|
||||
@@ -4937,7 +4938,7 @@ void main() {
|
||||
if (!(aspects & DrawSetupAspect::IndexBuffer) || pIndexBufferView == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
return true;
|
||||
}
|
||||
@@ -4981,7 +4982,7 @@ void main() {
|
||||
// the VALUE hash of that subset, never the version itself: the version is monotonic, so
|
||||
// per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds.
|
||||
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
|
||||
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
|
||||
@@ -5169,22 +5170,22 @@ void main() {
|
||||
syntheticVertexInputState.pNext = vis.state.pNext;
|
||||
pipelineVertexInputState = &syntheticVertexInputState;
|
||||
}
|
||||
auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);
|
||||
auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);
|
||||
auto cullFaceEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::CullFace);
|
||||
auto depthTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest);
|
||||
auto polygonOffsetFillEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&
|
||||
DrawModeUsesPolygonFill(mode);
|
||||
auto rasterizerDiscardEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);
|
||||
auto colorLogicOpEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;
|
||||
auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;
|
||||
auto stencilTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
// A framebuffer without a depth (stencil) attachment behaves as if the depth
|
||||
// (stencil) test always passes and nothing is written - even when the bound
|
||||
// image is a packed depth-stencil texture attached through only one half.
|
||||
{
|
||||
const auto& gatingFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) {
|
||||
const auto& depthAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Depth);
|
||||
const auto& stencilAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Stencil);
|
||||
@@ -5196,10 +5197,10 @@ void main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
|
||||
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
|
||||
const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front);
|
||||
const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back);
|
||||
const VkPolygonMode requestedPolygonMode =
|
||||
MG_Util::ConvertPolygonModeToVkEnum(MG_State::pGLContext->GetPolygonModeFront());
|
||||
MG_Util::ConvertPolygonModeToVkEnum(MGB_CTX->GetPolygonModeFront());
|
||||
// VK_POLYGON_MODE_LINE/_POINT require the fillModeNonSolid device feature; fall back to
|
||||
// VK_POLYGON_MODE_FILL when the device lacks it.
|
||||
const VkPolygonMode effectivePolygonMode =
|
||||
@@ -5284,18 +5285,18 @@ void main() {
|
||||
// driver's own rate. Both halves move the render state's PIPELINE version, so a cached
|
||||
// pipeline built at the old rate cannot be handed back for the new one.
|
||||
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
|
||||
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleShading),
|
||||
.minSampleShading = MGB_CTX->GetMinSampleShadingValue(),
|
||||
// Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63.
|
||||
.sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu},
|
||||
.subpass = 0,
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
||||
.patchControlPoints = static_cast<Uint32>(MGB_CTX->GetPatchVertices()),
|
||||
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
|
||||
.polygonMode = effectivePolygonMode,
|
||||
.cullMode = cullFaceEnabled
|
||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||
? MG_Util::ConvertCullFaceModeToVkEnum(MGB_CTX->GetCullFaceMode(), invertClockwise)
|
||||
: VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_CLOCKWISE,
|
||||
// Read the geometry stage off the program's own shader list rather than
|
||||
@@ -5309,13 +5310,13 @@ void main() {
|
||||
.provokingVertexMode = SelectProvokingVertexMode(
|
||||
vkTopology, ProgramCapturesXfbFromGeometryStage(program)),
|
||||
.depthTestEnable = depthTestEnabled,
|
||||
.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),
|
||||
.depthWriteEnable = depthTestEnabled && MGB_CTX->GetDepthMask(),
|
||||
.depthBiasEnable = polygonOffsetFillEnabled,
|
||||
.rasterizerDiscardEnable = rasterizerDiscardEnabled,
|
||||
.logicOpEnable = colorLogicOpEnabled,
|
||||
.stencilTestEnable = stencilTestEnabled,
|
||||
.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MG_State::pGLContext->GetDepthFunc()),
|
||||
.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MG_State::pGLContext->GetLogicOp()),
|
||||
.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MGB_CTX->GetDepthFunc()),
|
||||
.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MGB_CTX->GetLogicOp()),
|
||||
.frontStencilFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.FailOp),
|
||||
.frontStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthPassOp),
|
||||
.frontStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthFailOp),
|
||||
@@ -5351,8 +5352,8 @@ void main() {
|
||||
// handed back after the application changed them.
|
||||
if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable &&
|
||||
vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
|
||||
const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
const FloatVec4& defaultOuterLevel = MGB_CTX->GetPatchDefaultOuterLevel();
|
||||
const FloatVec2& defaultInnerLevel = MGB_CTX->GetPatchDefaultInnerLevel();
|
||||
payload.passthroughTessControlKey = ProgramFactory::ComputePassthroughTessControlKey(
|
||||
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
|
||||
programObj.passthroughPerVertexMembers);
|
||||
@@ -5404,7 +5405,7 @@ void main() {
|
||||
"GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity",
|
||||
payload.colorAttachmentCount);
|
||||
const auto& drawFboBinding =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null");
|
||||
const Bool isDefaultDrawFbo = drawFboBinding->IsDefaultFramebuffer();
|
||||
const auto& drawBuffers = drawFboBinding->GetDrawBuffers();
|
||||
@@ -5432,14 +5433,14 @@ void main() {
|
||||
BlendFactor dstAlpha = BlendFactor::Zero;
|
||||
BlendEquation colorEquation = BlendEquation::Add;
|
||||
BlendEquation alphaEquation = BlendEquation::Add;
|
||||
MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
|
||||
MG_State::pGLContext->GetBlendEquationIndexed(i, colorEquation, alphaEquation);
|
||||
const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
|
||||
MGB_CTX->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
|
||||
MGB_CTX->GetBlendEquationIndexed(i, colorEquation, alphaEquation);
|
||||
const Bool blendEnabled = MGB_CTX->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
|
||||
// Per-draw-buffer color write mask (glColorMaski). Divergent per-attachment masks require
|
||||
// the independentBlend device feature; when it is absent, fall back to draw buffer 0's
|
||||
// mask for every attachment (matching the non-indexed glColorMask broadcast).
|
||||
const BoolVec4 bufferMask =
|
||||
MG_State::pGLContext->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
|
||||
MGB_CTX->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
|
||||
VkColorComponentFlags attachmentColorWriteMask = static_cast<VkColorComponentFlags>(
|
||||
(bufferMask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) |
|
||||
(bufferMask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) |
|
||||
@@ -5855,7 +5856,7 @@ void main() {
|
||||
VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFbo) const {
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const auto& parameters = MGB_CTX->GetRenderStateParameters();
|
||||
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
|
||||
VkRect2D full{};
|
||||
full.offset = {0, 0};
|
||||
@@ -5916,7 +5917,7 @@ void main() {
|
||||
// One compare for the whole tail: see the gate's declaration in
|
||||
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
||||
// input the six Apply* below read.
|
||||
const Uint paramsVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
const Uint paramsVersion = MGB_CTX->GetRenderStateParametersVersion();
|
||||
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
|
||||
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
|
||||
shadow.dynamicTailIsDefaultFbo == isDefaultFbo) {
|
||||
@@ -5940,7 +5941,7 @@ void main() {
|
||||
// re-derive the value its shadow already holds.
|
||||
DynamicStateShadow::DynamicTailKey key;
|
||||
{
|
||||
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters();
|
||||
// Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two
|
||||
// (per-index state for indices > 0 is keyed separately, see multiViewportKey below).
|
||||
key.viewport[0] = p.Viewports[0].x();
|
||||
@@ -6016,7 +6017,7 @@ void main() {
|
||||
MOBILEGL_ASSERT(
|
||||
[&] {
|
||||
const auto& fbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
return isDefaultFbo == (fbo != nullptr && fbo->IsDefaultFramebuffer());
|
||||
}(),
|
||||
"GetBaseTransformFlagsRaw: isDefaultFbo does not match the bound draw framebuffer");
|
||||
@@ -6040,7 +6041,7 @@ void main() {
|
||||
// Entry select: by the draw program's lifetime id, MRU first (the id pins
|
||||
// the entry; every other fact is re-guarded below, so probing a stale
|
||||
// entry can only decline, never serve stale state).
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& program = *MGB_CTX->GetProgramForDraw();
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
SetupDrawSnapshot* snapPtr = nullptr;
|
||||
{
|
||||
@@ -6088,7 +6089,7 @@ void main() {
|
||||
// captured draw after glBeginTransformFeedback would bind the undecorated
|
||||
// variant and silently capture nothing while the CPU bookkeeping advances.
|
||||
const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled &&
|
||||
MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
MGB_CTX->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0;
|
||||
const Bool snapHasXfbCapture =
|
||||
static_cast<Bool>(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) &
|
||||
@@ -6102,12 +6103,12 @@ void main() {
|
||||
// buffer binds (re-run every draw anyway). Declining here would send every
|
||||
// draw of a VAO-cycling stream (Minecraft chunk rendering) through the full
|
||||
// path, re-resolving descriptors and texture layouts nothing invalidated.
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const Bool vaoMoved =
|
||||
static_cast<const void*>(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId ||
|
||||
vao.GetConfigVersion() != snap.vaoConfigVersion;
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
||||
drawFbo->GetLifetimeId() != snap.drawFboLifetimeId ||
|
||||
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
||||
@@ -6118,8 +6119,8 @@ void main() {
|
||||
// back on what the snapshot already describes (a GL_BLEND toggle between
|
||||
// two draws, a redundant glBindSampler), and declining here sends every
|
||||
// such draw through the full SetupDraw.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
|
||||
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
|
||||
const Bool renderStateMoved = renderStateVersion != snap.renderStateVersion;
|
||||
const Bool bindsMoved = bindGeneration != snap.bindGeneration;
|
||||
if (renderStateMoved) {
|
||||
@@ -6127,7 +6128,7 @@ void main() {
|
||||
// flavor input (depth/stencil participation); a flip of that must take
|
||||
// the full path's pass selection. One bulk parameters fetch instead of
|
||||
// two capability-accessor calls; both are pure reads of the same fields.
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
|
||||
const Bool drawUsesDepthStencil = rsp.DepthTestEnabled || rsp.StencilTestEnabled;
|
||||
if (drawUsesDepthStencil != snap.drawUsesDepthStencil) {
|
||||
return false;
|
||||
@@ -6301,7 +6302,7 @@ void main() {
|
||||
if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) {
|
||||
return false;
|
||||
}
|
||||
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Uint64 samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
|
||||
// Decline, not re-arm: snap.resolvedTransformFlags bakes the
|
||||
// ExplicitLod0Sampling verdict, which reads the effective sampler's
|
||||
@@ -6431,7 +6432,7 @@ void main() {
|
||||
// was cancelled has no usable optimized module - and on an in-place
|
||||
// SanitizeAndOptimizeBinary failure GetGeneratedSpirv() still holds the RAW
|
||||
// glslang words, which must never reach vkCreateShaderModule. Drop the draw.
|
||||
const auto& drawProgram = *MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& drawProgram = *MGB_CTX->GetProgramForDraw();
|
||||
if (!drawProgram.GetLinkStatus() || !drawProgram.GetSpirvStatus()) {
|
||||
MGLOG_D("SetupDraw skipped: program=%u is linked=%d spirv=%d",
|
||||
drawProgram.GetExternalIndex(), static_cast<int>(drawProgram.GetLinkStatus()),
|
||||
@@ -6461,15 +6462,15 @@ void main() {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
|
||||
}
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {
|
||||
// Nothing was mutated: other entries' per-probe guards (FBO identity +
|
||||
// version among them) stay authoritative, so none need invalidating.
|
||||
RecordUnsupportedFramebufferError(__func__);
|
||||
return false;
|
||||
}
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const auto& program = *MGB_CTX->GetProgramForDraw();
|
||||
// The fast path declined (or had no entry for this program): whatever THIS
|
||||
// program's entry saw may be stale, and the full path below mutates state as
|
||||
// it goes, so the entry must not stay matchable if that path fails mid-way.
|
||||
@@ -6509,7 +6510,7 @@ void main() {
|
||||
ProgramFactory::CompileOptionFlags transformFlags =
|
||||
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(drawFboIsDefault));
|
||||
// Captured draws take the xfb-decorated program variant.
|
||||
if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
if (m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0) {
|
||||
transformFlags |= ProgramFactory::CompileOptionBit::XfbCapture;
|
||||
}
|
||||
@@ -6523,13 +6524,13 @@ void main() {
|
||||
{
|
||||
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
|
||||
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Uint64 lodBindGeneration = MGB_CTX->GetTextureBindGeneration();
|
||||
// The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range
|
||||
// (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the
|
||||
// sampling-resolution generation - not the texture params version the sum
|
||||
// below covers. Without this key a filter/aniso change would keep serving
|
||||
// the stale verdict.
|
||||
const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Uint64 lodSamplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
Bool lodMemoHit = false;
|
||||
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
|
||||
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
|
||||
@@ -6647,7 +6648,7 @@ void main() {
|
||||
{
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 programVersion = program.GetBackendStateVersion();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
|
||||
// The bind generation alone stopped covering this set the moment ResolveSampledBinding
|
||||
// started asking SamplesAsIncompleteTexture: membership now depends on the effective
|
||||
// sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on
|
||||
@@ -6666,7 +6667,7 @@ void main() {
|
||||
// object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind
|
||||
// generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo
|
||||
// already carry.
|
||||
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Uint64 samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
const Bool sampledSetUnchanged =
|
||||
m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId &&
|
||||
m_lastSampledSetProgramVersion == programVersion &&
|
||||
@@ -6827,8 +6828,8 @@ void main() {
|
||||
// pass flavor (GL: a disabled depth/stencil test neither reads nor writes
|
||||
// its buffer).
|
||||
const Bool drawUsesDepthStencil =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
auto* renderPassEntry =
|
||||
m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
|
||||
// nullptr: the framebuffer has an attachment DirectVulkan cannot represent (a texture the
|
||||
@@ -6969,8 +6970,8 @@ void main() {
|
||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||
snap.drawFboIsDefault = drawFboIsDefault;
|
||||
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
snap.renderStateVersion = MGB_CTX->GetPipelineStateVersion();
|
||||
snap.bindGeneration = MGB_CTX->GetTextureBindGeneration();
|
||||
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
||||
snap.resolvedTransformFlags = transformFlags.GetRaw();
|
||||
snap.renderPassHash = nowActiveRenderPass->hash;
|
||||
@@ -6996,7 +6997,7 @@ void main() {
|
||||
snap.programObj = nullptr;
|
||||
snap.programFactoryEpoch = 0;
|
||||
}
|
||||
snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
snap.samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration();
|
||||
Uint64 snapContentSum = 0;
|
||||
Uint64 snapParamsSum = 0;
|
||||
// Per-entry copies of this draw's sampled set (the scratch vectors
|
||||
@@ -7033,7 +7034,7 @@ void main() {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
|
||||
// itself, never the graphics composite (which carries no compute stage at all).
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
|
||||
const auto& program = *MGB_CTX->GetProgramForDispatch();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E_ONCE("DispatchCompute skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
@@ -7085,7 +7086,7 @@ void main() {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
// See DispatchCompute: the dispatch accessor, not the draw one.
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
|
||||
const auto& program = *MGB_CTX->GetProgramForDispatch();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
@@ -7129,7 +7130,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
|
||||
auto indirectBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
|
||||
if (!indirectBuffer) {
|
||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: GL_DISPATCH_INDIRECT_BUFFER is not bound");
|
||||
return;
|
||||
@@ -7203,10 +7204,10 @@ void main() {
|
||||
|
||||
VkClearRect clearRect{};
|
||||
clearRect.rect = framebuffer.IsDefaultFramebuffer()
|
||||
? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),
|
||||
? MakeDefaultFramebufferScissorRect(MGB_CTX->GetScissorBox(),
|
||||
renderPassEntry->extent,
|
||||
m_swapchainObject.GetPreTransform())
|
||||
: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);
|
||||
: MakeClampedScissorRect(MGB_CTX->GetScissorBox(), renderPassEntry->extent);
|
||||
clearRect.baseArrayLayer = 0;
|
||||
// GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer.
|
||||
clearRect.layerCount = renderPassEntry->layers;
|
||||
@@ -7254,10 +7255,10 @@ void main() {
|
||||
return;
|
||||
}
|
||||
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return;
|
||||
}
|
||||
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)");
|
||||
if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) {
|
||||
RecordUnsupportedFramebufferError(__func__);
|
||||
@@ -7265,16 +7266,16 @@ void main() {
|
||||
}
|
||||
|
||||
ClearFramebufferPayload payload {
|
||||
.color = MG_State::pGLContext->GetClearColor(),
|
||||
.depth = MG_State::pGLContext->GetClearDepth(),
|
||||
.stencil = MG_State::pGLContext->GetClearStencil()
|
||||
.color = MGB_CTX->GetClearColor(),
|
||||
.depth = MGB_CTX->GetClearDepth(),
|
||||
.stencil = MGB_CTX->GetClearStencil()
|
||||
};
|
||||
|
||||
// A render-pass loadOp clear always covers the complete attachment, while
|
||||
// OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for
|
||||
// GuiItemAtlas: animated items clear only their atlas slot before being
|
||||
// redrawn. Queueing that clear as a loadOp erases every cached static item.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
VkClearRect clearRect{};
|
||||
switch (PrepareScissoredClear(*fbo, clearRect)) {
|
||||
case ScissoredClearPrep::NoOp:
|
||||
@@ -7297,7 +7298,7 @@ void main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
continue;
|
||||
}
|
||||
@@ -7324,7 +7325,7 @@ void main() {
|
||||
}
|
||||
|
||||
VkImageAspectFlags depthStencilAspects = 0;
|
||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {
|
||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask()) {
|
||||
const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsComplete()) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
@@ -7337,7 +7338,7 @@ void main() {
|
||||
// vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or
|
||||
// zero mask can be expressed; treat a partial mask like a partial color mask.
|
||||
const Uint32 stencilWriteMask =
|
||||
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
} else if (stencilWriteMask != 0) {
|
||||
@@ -7366,11 +7367,11 @@ void main() {
|
||||
// gating for the deferred path: drop fully-masked planes, warn on partial
|
||||
// masks vkCmdClear*/loadOp clears cannot express.
|
||||
GLbitfield deferredMask = mask;
|
||||
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {
|
||||
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MGB_CTX->GetDepthMask()) {
|
||||
deferredMask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) != 0xFFu) {
|
||||
if (stencilWriteMask != 0) {
|
||||
MGLOG_W_ONCE("DirectVulkan: deferred glClear with a partial stencil write mask is not supported");
|
||||
@@ -7386,7 +7387,7 @@ void main() {
|
||||
if (drawBuffers[drawBufferIndex] == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
|
||||
const Bool full = colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a();
|
||||
if (full) {
|
||||
anyFullMask = true;
|
||||
@@ -7407,7 +7408,7 @@ void main() {
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
|
||||
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
|
||||
continue;
|
||||
}
|
||||
@@ -7436,7 +7437,7 @@ void main() {
|
||||
const ClearAttachmentPayload& clearPayload) {
|
||||
m_clearManager->CollectGarbage();
|
||||
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return;
|
||||
}
|
||||
if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) {
|
||||
@@ -7478,7 +7479,7 @@ void main() {
|
||||
}
|
||||
|
||||
// GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
VkClearRect clearRect{};
|
||||
switch (PrepareScissoredClear(framebuffer, clearRect)) {
|
||||
case ScissoredClearPrep::NoOp:
|
||||
@@ -7509,9 +7510,9 @@ void main() {
|
||||
|
||||
// GL 3.3 §4.2.3: ClearBuffer* honors the write masks like Clear. Deferred
|
||||
// clears cannot express partial masks; warn and skip those.
|
||||
const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };
|
||||
const auto depthClearAllowed = [&]() -> Bool { return MGB_CTX->GetDepthMask(); };
|
||||
const auto stencilClearAllowed = [&]() -> Bool {
|
||||
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
return true;
|
||||
}
|
||||
@@ -7523,7 +7524,7 @@ void main() {
|
||||
|
||||
switch (buffer) {
|
||||
case GL_COLOR: {
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
|
||||
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
return;
|
||||
}
|
||||
@@ -7580,7 +7581,7 @@ void main() {
|
||||
if (!attachment.IsComplete()) {
|
||||
return;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
|
||||
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
return;
|
||||
}
|
||||
@@ -7598,7 +7599,7 @@ void main() {
|
||||
MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(colorTexture));
|
||||
} else {
|
||||
VkImageAspectFlags aspects = 0;
|
||||
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&
|
||||
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask() &&
|
||||
framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) {
|
||||
aspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
@@ -7606,7 +7607,7 @@ void main() {
|
||||
framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) {
|
||||
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear).
|
||||
const Uint32 stencilWriteMask =
|
||||
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
aspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
} else if (stencilWriteMask != 0) {
|
||||
@@ -7625,7 +7626,7 @@ void main() {
|
||||
|
||||
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload) {
|
||||
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
if (!fbo) {
|
||||
return;
|
||||
}
|
||||
@@ -8583,8 +8584,8 @@ void main() {
|
||||
void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter) {
|
||||
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
|
||||
@@ -8616,8 +8617,8 @@ void main() {
|
||||
|
||||
// The scissor test clips blit writes: intersect the destination rectangle with
|
||||
// the scissor box and shrink the source proportionally.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
const IntVec4& scissor = MG_State::pGLContext->GetScissorBox();
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
const IntVec4& scissor = MGB_CTX->GetScissorBox();
|
||||
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
|
||||
const Bool dstFlipped = d1 < d0;
|
||||
GLint lo = dstFlipped ? d1 : d0;
|
||||
@@ -9211,7 +9212,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
|
||||
auto destinationTexture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
if (destinationTexture == nullptr) {
|
||||
RecordTextureCopyError(__func__, ErrorCode::InvalidOperation,
|
||||
@@ -9219,7 +9220,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (readFbo == nullptr) {
|
||||
RecordTextureCopyError(__func__, ErrorCode::InvalidOperation,
|
||||
"CopyTexSubImage2D requires a framebuffer bound to GL_READ_FRAMEBUFFER.");
|
||||
@@ -9930,7 +9931,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (readFbo == nullptr) {
|
||||
MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: no read framebuffer is bound");
|
||||
return;
|
||||
@@ -10685,8 +10686,8 @@ void main() {
|
||||
|
||||
// Store honoring the client pack state (single slice).
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT packAlignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
|
||||
const SizeT dstRowStride = ((rowPixels * dstPixelBytes) + packAlignment - 1) / packAlignment * packAlignment;
|
||||
@@ -10716,7 +10717,7 @@ void main() {
|
||||
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& activeUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
|
||||
auto textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
GetTextureImage(textureObject, textureUploadTarget, level, format, type, -1, pixels);
|
||||
}
|
||||
@@ -10960,7 +10961,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
|
||||
auto texture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
|
||||
MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture.");
|
||||
@@ -11216,7 +11217,7 @@ void main() {
|
||||
// resets its counter state, because those bytes describe the previous owner's span.
|
||||
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
|
||||
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
|
||||
const Uint64 identity = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
|
||||
const Uint64 identity = MGB_CTX->GetBoundTransformFeedbackLifetimeId();
|
||||
MOBILEGL_ASSERT(identity != 0,
|
||||
"transform feedback object reported the free-slot sentinel (0) as its identity - "
|
||||
"every slot would then read as 'mine' without ever being claimed");
|
||||
@@ -11233,7 +11234,7 @@ void main() {
|
||||
Uint32 slot = freeSlot;
|
||||
if (slot == kNoSlot) {
|
||||
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
|
||||
if (MG_State::pGLContext->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
|
||||
if (MGB_CTX->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
|
||||
continue;
|
||||
}
|
||||
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
|
||||
@@ -11264,17 +11265,17 @@ void main() {
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BeginXfbCaptureForDraw(FrameContext::FrameData& frame) {
|
||||
if (!m_transformFeedbackFeatureEnabled || MG_State::pGLContext == nullptr ||
|
||||
!MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
if (!m_transformFeedbackFeatureEnabled || !MGB_CTX_LIVE ||
|
||||
!MGB_CTX->IsTransformFeedbackActive()) {
|
||||
return false;
|
||||
}
|
||||
// A paused span captures nothing, and the counter buffers keep their values, so the
|
||||
// next resumed draw appends exactly where the last captured one stopped - which is
|
||||
// what pause/resume means (ARB_transform_feedback2).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||
if (MGB_CTX->IsTransformFeedbackPaused()) {
|
||||
return false;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
|
||||
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -11313,7 +11314,7 @@ void main() {
|
||||
VkDeviceSize offsets[4] = {};
|
||||
VkDeviceSize sizes[4] = {};
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
const auto& bufferObject = point.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
@@ -11344,7 +11345,7 @@ void main() {
|
||||
offsets, sizes);
|
||||
|
||||
const Uint32 counterSlot = CurrentXfbCounterSlot();
|
||||
const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration();
|
||||
const Uint64 generation = MGB_CTX->GetTransformFeedbackGeneration();
|
||||
const Bool resume = m_xfbCountersValid[counterSlot] && m_xfbLastSeenGeneration[counterSlot] == generation;
|
||||
m_xfbLastSeenGeneration[counterSlot] = generation;
|
||||
|
||||
@@ -11367,7 +11368,7 @@ void main() {
|
||||
if (!began) {
|
||||
return;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
|
||||
const SizeT bufferCount = program ? std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4) : 0;
|
||||
const Uint32 counterSlot = CurrentXfbCounterSlot();
|
||||
VkBuffer counterBuffers[4] = {};
|
||||
@@ -12015,7 +12016,7 @@ void main() {
|
||||
default: break;
|
||||
}
|
||||
if (mergeGranularity != 0) {
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartEnabled || rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
mergeGranularity = 0;
|
||||
}
|
||||
@@ -12093,7 +12094,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get();
|
||||
if (!indexBuffer) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no element array buffer is bound");
|
||||
@@ -12103,13 +12104,13 @@ void main() {
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
const SizeT commandBytes = commandOffset +
|
||||
static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) + kGLDrawElementsIndirectCommandBytes;
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
@@ -12194,7 +12195,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get();
|
||||
if (!indexBuffer) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: no element array buffer is bound");
|
||||
@@ -12204,7 +12205,7 @@ void main() {
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
const SizeT commandBytes = commandOffset +
|
||||
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + kGLDrawElementsIndirectCommandBytes;
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
@@ -12274,7 +12275,7 @@ void main() {
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
const SizeT commandBytes = commandOffset +
|
||||
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + kGLDrawArraysIndirectCommandBytes;
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
@@ -12763,8 +12764,8 @@ void main() {
|
||||
if (!m_provokingVertexModePerPipeline) {
|
||||
return VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
|
||||
}
|
||||
return (MG_State::pGLContext != nullptr &&
|
||||
MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
|
||||
return (MGB_CTX_LIVE &&
|
||||
MGB_CTX->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
|
||||
? VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT
|
||||
: VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -527,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);
|
||||
}
|
||||
|
||||
@@ -535,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);
|
||||
}
|
||||
|
||||
@@ -544,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);
|
||||
}
|
||||
|
||||
@@ -553,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);
|
||||
}
|
||||
@@ -562,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);
|
||||
}
|
||||
|
||||
@@ -570,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);
|
||||
}
|
||||
|
||||
@@ -579,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);
|
||||
}
|
||||
|
||||
@@ -588,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);
|
||||
}
|
||||
|
||||
@@ -596,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);
|
||||
}
|
||||
|
||||
@@ -605,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);
|
||||
}
|
||||
@@ -615,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);
|
||||
}
|
||||
@@ -625,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);
|
||||
}
|
||||
@@ -635,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);
|
||||
}
|
||||
|
||||
@@ -645,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);
|
||||
}
|
||||
@@ -655,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);
|
||||
}
|
||||
@@ -665,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);
|
||||
}
|
||||
@@ -675,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);
|
||||
}
|
||||
|
||||
@@ -683,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,
|
||||
@@ -691,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);
|
||||
}
|
||||
@@ -700,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);
|
||||
}
|
||||
|
||||
@@ -708,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);
|
||||
}
|
||||
|
||||
@@ -739,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);
|
||||
}
|
||||
|
||||
@@ -791,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -812,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);
|
||||
}
|
||||
}
|
||||
@@ -882,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);
|
||||
}
|
||||
|
||||
@@ -903,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);
|
||||
}
|
||||
|
||||
@@ -916,6 +943,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support regional memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrierByRegion);
|
||||
memoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
@@ -1238,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);
|
||||
}
|
||||
}
|
||||
@@ -1320,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();
|
||||
@@ -1328,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);
|
||||
}
|
||||
}
|
||||
@@ -1349,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();
|
||||
}
|
||||
}
|
||||
@@ -1363,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();
|
||||
}
|
||||
}
|
||||
@@ -1568,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);
|
||||
@@ -1599,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 {
|
||||
@@ -164,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;
|
||||
@@ -178,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;
|
||||
@@ -257,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
|
||||
@@ -271,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;
|
||||
@@ -286,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;
|
||||
}
|
||||
@@ -297,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
|
||||
@@ -317,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;
|
||||
@@ -422,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;
|
||||
@@ -441,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;
|
||||
@@ -519,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);
|
||||
@@ -527,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;
|
||||
}
|
||||
@@ -579,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);
|
||||
}
|
||||
}
|
||||
@@ -588,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;
|
||||
@@ -604,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;
|
||||
@@ -642,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;
|
||||
@@ -771,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;
|
||||
@@ -912,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);
|
||||
}
|
||||
|
||||
@@ -6453,6 +6460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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;
|
||||
@@ -6657,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
|
||||
@@ -127,6 +127,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/PipeVerifyArmingScenario.cpp
|
||||
Scenarios/PoisonOmissionScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -631,3 +633,181 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
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,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,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
|
||||
@@ -31,11 +31,13 @@
|
||||
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 once the */ \
|
||||
/* inventory carries the target argument (P1). Named for the plan's explicit */ \
|
||||
/* 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) \
|
||||
@@ -94,7 +96,30 @@
|
||||
X(IsTransformFeedbackPaused, PauseStreamOutput) \
|
||||
X(InvalidateCompileEnv, kClientResolved) \
|
||||
X(ValidateProgramName, kClientResolved) \
|
||||
X(RecordError, kReverseChannel)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -85,6 +85,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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"
|
||||
|
||||
|
||||
@@ -22,19 +22,20 @@
|
||||
// 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, recorded here so it is impossible to miss: two payloads reach into headers
|
||||
// this directory is eventually forbidden to see - MGPCaps embeds MG_Backend's
|
||||
// DynamicBackendParameters, and ResidualValueBlock embeds MG_State's RenderStateParameters
|
||||
// and PixelStoreParameters. Both are deliberate: the caps block IS that struct (section
|
||||
// 4.4.1) and the residual block is the migration carrier for Track V (section 6.3). P0.5
|
||||
// extracts MGPipeValueTypes.h and both includes below go away; until then purity gate A
|
||||
// (section 10.3) cannot be armed for this header.
|
||||
// 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 <MG_State/GLState/RenderState/RenderState.h>
|
||||
#include "MGPipeValueTypes.h"
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
using MG_Backend::DynamicBackendParameters;
|
||||
// Both live directly in namespace MobileGL today; P0.5 moves them into
|
||||
// Both live directly in namespace MobileGL and, since P0.5, are declared in
|
||||
// MG_Pipe/MGPipeValueTypes.h.
|
||||
using MobileGL::PixelStoreParameters;
|
||||
using MobileGL::RenderStateParameters;
|
||||
|
||||
@@ -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
|
||||
@@ -12,9 +12,11 @@
|
||||
// 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. Adding a field to a payload without adding it
|
||||
// here makes the comparator blind to it; that gap closes in P1, when the verify harness
|
||||
// goes live and the comparator's coverage is itself asserted.
|
||||
// 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
|
||||
|
||||
@@ -218,6 +220,77 @@
|
||||
#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) \
|
||||
@@ -233,6 +306,8 @@
|
||||
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(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,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");
|
||||
@@ -22,8 +22,8 @@
|
||||
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
|
||||
// Fatal{UnmigratedPipeInput} (section 6.2.2).
|
||||
//
|
||||
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
|
||||
// itself lands in P1.
|
||||
// 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,
|
||||
@@ -87,11 +87,13 @@ enum class MGPipeInputField : Uint16 {
|
||||
InvalidateCompileEnv,
|
||||
ValidateProgramName,
|
||||
RecordError,
|
||||
GetBoundTransformFeedbackLifetimeId,
|
||||
HasOpenTransformFeedbackSpan,
|
||||
kFieldCount,
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
|
||||
static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");
|
||||
static_assert(kMGPipeInputFieldCount == 63, "the PipeInputs field set moved");
|
||||
|
||||
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
|
||||
"GetActiveTextureUnit",
|
||||
@@ -155,11 +157,13 @@ inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
|
||||
"InvalidateCompileEnv",
|
||||
"ValidateProgramName",
|
||||
"RecordError",
|
||||
"GetBoundTransformFeedbackLifetimeId",
|
||||
"HasOpenTransformFeedbackSpan",
|
||||
};
|
||||
|
||||
// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each
|
||||
// true has to be argued for in P1 when the fillers land: a sticky field is a field
|
||||
// the poison cannot protect.
|
||||
// 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
|
||||
@@ -169,7 +173,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
|
||||
false, // GetBoundVertexArray
|
||||
false, // GetBufferBindingSlot
|
||||
false, // GetBufferBindingPoint
|
||||
false, // GetBufferBindingPointCount
|
||||
true, // GetBufferBindingPointCount: keyed by target: a constexpr capacity table, not verb state
|
||||
false, // GetTouchedBufferBindingPointCount
|
||||
false, // GetClampReadColor
|
||||
false, // GetClearColor
|
||||
@@ -198,7 +202,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
|
||||
false, // GetPrimitiveRestartIndex
|
||||
false, // GetProgramForDispatch
|
||||
false, // GetProgramForDraw
|
||||
false, // GetProgramObject
|
||||
true, // GetProgramObject: keyed by GL name: an object lookup, not verb state
|
||||
false, // GetProvokingVertexMode
|
||||
false, // GetRenderStateParameters
|
||||
false, // GetRenderStateParametersVersion
|
||||
@@ -207,7 +211,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
|
||||
false, // GetStencilState
|
||||
false, // GetTextureBindGeneration
|
||||
false, // GetTextureContextId
|
||||
false, // GetTextureObject
|
||||
true, // GetTextureObject: keyed by GL name: an object lookup, not verb state
|
||||
false, // GetTextureUnitObject
|
||||
false, // GetTransformFeedbackCapturedVertices
|
||||
false, // GetTransformFeedbackGeneration
|
||||
@@ -219,10 +223,13 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
|
||||
false, // IsCapabilityEnabledIndexed
|
||||
false, // IsTransformFeedbackActive
|
||||
false, // IsTransformFeedbackPaused
|
||||
false, // InvalidateCompileEnv
|
||||
false, // ValidateProgramName
|
||||
false, // RecordError
|
||||
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.
|
||||
@@ -288,6 +295,8 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
|
||||
"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 {
|
||||
@@ -301,8 +310,12 @@ struct MGPipeFilledState {
|
||||
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);
|
||||
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
|
||||
: state.FilledGen[index] == state.CurrentVerbSerial;
|
||||
const Uint64 gen = state.FilledGen[index];
|
||||
if (gen == 0) return false;
|
||||
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,23 @@
|
||||
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);
|
||||
@@ -90,6 +107,12 @@ inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outF
|
||||
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 {};
|
||||
@@ -217,12 +240,26 @@ 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>) {
|
||||
@@ -230,15 +267,23 @@ inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
||||
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
|
||||
return a == b;
|
||||
} else {
|
||||
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
|
||||
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
|
||||
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
|
||||
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
|
||||
// and this branch stops being reachable from any payload.
|
||||
return std::memcmp(&a, &b, sizeof(T)) == 0;
|
||||
// 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) {
|
||||
@@ -568,6 +613,36 @@ inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const
|
||||
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 = 63;
|
||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
enum class FramebufferTarget {
|
||||
@@ -103,7 +104,7 @@ namespace MobileGL {
|
||||
|
||||
class FramebufferObject {
|
||||
public:
|
||||
static constexpr Uint MAX_DRAW_BUFFERS = 8;
|
||||
static constexpr Uint MAX_DRAW_BUFFERS = MobileGL::kMGMaxDrawBuffers;
|
||||
|
||||
using TargetEnum = FramebufferTarget;
|
||||
using FramebufferAttachmentObjectArray =
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.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> // String/Vector/Array/UnorderedMap/SharedPtr + GL enums. DEBT NOTE (MGPipeTypes.h:24-31 style):
|
||||
// Includes.h:80-84 still pulls glslang and :59 spirv_cross_c.h; this header is glslang-free BY
|
||||
// SYMBOL (what P7's `nm -D | grep glslang` measures), not by preprocessed text. A textually
|
||||
// glslang-free closure needs MG_Util/Types.h split off Includes.h (Types.h:11 includes it
|
||||
// back) - out of scope for P0.5.
|
||||
#include <set> // std::set<String> (LinkArtifacts); NOT provided by Includes.h on its own terms
|
||||
// PURITY: no ShaderObject.h, no SpvcSession.h, nothing under MG_Util/ShaderTranspiler/, no Config.h,
|
||||
// no MG_Backend/, no BufferState/. scripts/check_include_closure.py probe "artifacts-header" (ROADMAP P0.5;
|
||||
// ARCHITECTURE.md:260) asserts that closure. The glslang scope token appears exactly twice below (B.0 D5: the two
|
||||
// glslang-typed LinkArtifacts members moved verbatim) and the gate pins that count.
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
// Namespace scope so SpirvArtifacts::reservedNumSamplesOffset can default to it;
|
||||
// ProgramObject::kInvalidUniformOffset is defined from this one.
|
||||
inline constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
|
||||
// Everything the query surface ever asked a glslang TType, flattened. Twenty
|
||||
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||
// AST, so a POD covers the whole surface exactly.
|
||||
struct TypeFacts {
|
||||
Bool isArray = false;
|
||||
// A runtime-sized array (a storage block's unsized trailing member) is an array
|
||||
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
|
||||
Bool isSizedArray = false;
|
||||
Bool isMatrix = false;
|
||||
Bool isVector = false;
|
||||
Bool isOpaque = false;
|
||||
Bool isTexture = false;
|
||||
Bool isImage = false;
|
||||
Bool isDouble = false; // getBasicType() == EbtDouble
|
||||
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
|
||||
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
|
||||
Bool isPatch = false; // getQualifier().patch
|
||||
Bool hasIndex = false; // getQualifier().hasIndex()
|
||||
Bool hasFormat = false; // getQualifier().hasFormat()
|
||||
Int vectorSize = 0;
|
||||
Int matrixCols = 0;
|
||||
Int matrixRows = 0;
|
||||
Int layoutIndex = 0; // getQualifier().layoutIndex
|
||||
Uint layoutFormat = 0; // getQualifier().getFormat()
|
||||
// glslang TLayoutMatrix, widened. For a uniform this is already RESOLVED against
|
||||
// the owning block's qualifier, so the getUniformBlock() fallback the old
|
||||
// accessors carried is gone.
|
||||
Int layoutMatrix = 0;
|
||||
// glslang TBasicType, widened - ApplyUniformInitialValues and the typed
|
||||
// glGetUniform* paths compare against a handful of enumerators.
|
||||
Int basicType = 0;
|
||||
};
|
||||
|
||||
// One glslang TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
|
||||
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
|
||||
struct ResourceReflection {
|
||||
String name;
|
||||
GLenum glDefineType = 0;
|
||||
Int offset = -1;
|
||||
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
|
||||
// the resolved GL_UNIFORM_SIZE answer.
|
||||
Int size = 0;
|
||||
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
|
||||
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
|
||||
Int index = -1;
|
||||
Int counterIndex = -1;
|
||||
Int arrayStride = 0;
|
||||
Int topLevelArraySize = 0;
|
||||
Int topLevelArrayStride = 0;
|
||||
Int binding = -1;
|
||||
Int location = -1; // layoutLocation()
|
||||
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
|
||||
// nobody", which is what the dead-default-block-uniform filter tests.
|
||||
Uint32 stages = 0;
|
||||
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
|
||||
// isSizedArray()/getOuterArraySize()/size fallback.
|
||||
GLint arraySize = 1;
|
||||
TypeFacts type;
|
||||
};
|
||||
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
using PipeOutputReflection = ResourceReflection;
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
// Offset within the gap-free record a backend that cannot express the GL
|
||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||
Uint32 packedOffsetBytes = 0;
|
||||
|
||||
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
||||
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
||||
// what the interface queries and the ESSL backend's driver-side capture list
|
||||
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
||||
// the three fields below carry what a SPIR-V backend needs instead: the
|
||||
// decoration target is the block's *instance* variable and the member index
|
||||
// inside it. blockMemberIndex < 0 means "not a block member".
|
||||
String blockInstanceName;
|
||||
String blockName;
|
||||
Int blockMemberIndex = -1;
|
||||
// Which element of an arrayed block member this capture names, -1 for "the
|
||||
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
||||
// backend needs the element index to tell a full run from a partial one.
|
||||
Int blockMemberElement = -1;
|
||||
};
|
||||
|
||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||
//
|
||||
// The membership rule is mechanical, not editorial: this is exactly the field list
|
||||
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
|
||||
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
|
||||
// as much link output). Nothing else belongs here.
|
||||
//
|
||||
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
|
||||
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
|
||||
// of thirty cross-thread field assignments. Until then this is a pure refactor.
|
||||
//
|
||||
// Access rule (invariant I5): the member below is private and reachable ONLY
|
||||
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
|
||||
// what makes "every read of link output joins the pending link" a property the
|
||||
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||
// without going through the gate. m_artifacts lives in ProgramObject and is private
|
||||
// there; the type being namespace-scope changes nothing about that gate.
|
||||
// ---- the owned mirror of glslang's reflection ----
|
||||
//
|
||||
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
|
||||
// asking the live glslang TProgram - program->getUniform(i).getType()->isMatrix()
|
||||
// and friends. That made the TProgram part of the program's PERMANENT state, which
|
||||
// in turn made the whole front end (parse + link) unskippable: the L1 shader
|
||||
// translation memo could hand back the SPIR-V but the reflection still had to be
|
||||
// rebuilt from a freshly parsed AST.
|
||||
//
|
||||
// These three tables are a snapshot of everything the query surface ever reads off
|
||||
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
|
||||
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
|
||||
// they are copyable, immutable after the link, and safe to memoize and share between
|
||||
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
|
||||
// everything except DoReflection itself.
|
||||
//
|
||||
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
|
||||
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
|
||||
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
|
||||
// instead, unchanged in every other respect.
|
||||
|
||||
struct LinkArtifacts {
|
||||
// Live only between LinkProgram() and the end of DoReflection. Everything after
|
||||
// that reads the owned mirror below; a link served from the L1 memo never
|
||||
// constructs one at all, so this is null for such a program and MUST NOT be
|
||||
// dereferenced outside DoReflection.
|
||||
SharedPtr<glslang::TProgram> program;
|
||||
|
||||
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
|
||||
Vector<UniformReflection> uniformReflection;
|
||||
Vector<BlockReflection> blockReflection;
|
||||
Vector<PipeInputReflection> pipeInputReflection;
|
||||
Vector<PipeOutputReflection> pipeOutputReflection;
|
||||
// Program-level scalars glslang answers off the linked intermediates.
|
||||
// Whether the program's LAST stage is the fragment stage. A color number - and so a
|
||||
// color index - exists only there; a separable tess/geometry/vertex program's
|
||||
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||
// separate-programs-tess-control).
|
||||
Bool lastStageIsFragment = false;
|
||||
Array<GLuint, 3> computeLocalSize{};
|
||||
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||
// TProgram uniform index.
|
||||
UnorderedMap<String, Int> uniformIndexByName;
|
||||
|
||||
// Attributes (Vertex in)
|
||||
Vector<String> attribs;
|
||||
Vector<GLenum> attribTypes;
|
||||
|
||||
// FragData (Frag out): the per-link snapshot of the explicit request maps.
|
||||
UnorderedMap<String, Uint> linkedFragDataLocation;
|
||||
UnorderedMap<String, Uint> linkedFragDataIndex;
|
||||
|
||||
// GL-facing index spaces (see the translation helpers above): GL active-uniform
|
||||
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
|
||||
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
|
||||
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
|
||||
Vector<Int> glUniformIndexToTProgram;
|
||||
Vector<Int> tProgramUniformIndexToGl;
|
||||
Vector<Int> glBlockIndexToTProgram;
|
||||
Vector<Int> tProgramBlockIndexToGl;
|
||||
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
|
||||
// glBlockIndexToTProgram above.
|
||||
//
|
||||
// That list is the BLOCK space - everything the relaxed parse produced except
|
||||
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
|
||||
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
|
||||
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
|
||||
// through indexToUniformBlock too, and the list therefore also carries every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
|
||||
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
|
||||
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
|
||||
//
|
||||
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
|
||||
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
|
||||
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
|
||||
// DirectGLES.cpp), so compacting that list would renumber every backend binding
|
||||
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
|
||||
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||
// DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
// reads until the application overwrites it, and relinking restores it - but the
|
||||
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
|
||||
// cannot carry an initializer, so the value only survives as this side-channel.
|
||||
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
|
||||
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
|
||||
UnorderedMap<String, Uint> uniformLocations;
|
||||
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
|
||||
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
|
||||
// write (GL resets uniforms to their initial values), so living here means the set
|
||||
// is cleared by the same three paths that clear the rest of a link's output -
|
||||
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
|
||||
// fourth reset site can be forgotten. Empty (and never allocated) for a program
|
||||
// that never asked to be separable.
|
||||
Vector<Uint64> writtenUniformLocationBits;
|
||||
Vector<Uint64> writtenUniformIndexBits;
|
||||
Vector<Uint> writtenUniformIndices;
|
||||
// Ordered by location,
|
||||
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||
Vector<Int> uniformIndexInTProgram;
|
||||
// ditto. Will be set at glUniform1i
|
||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||
// Sampler/image layout(binding = N) initial texture/image units, captured by
|
||||
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
|
||||
// qualifier still says what the shader declared. An OUTPUT of the link, not an
|
||||
// input to it: nothing supplies this map, the resolver fills it.
|
||||
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||
|
||||
// Ordered by uniform block index
|
||||
// index is DIFFERENT from binding!!!
|
||||
//
|
||||
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
||||
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
||||
// `prog->getUniformBlock(i) == "BlockName"`
|
||||
// These stuff are present for GL semantics, not for backend inspection
|
||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
Int uniformBlockNameMaxLength = 0;
|
||||
|
||||
String infoLog;
|
||||
Bool linkStatus = false;
|
||||
|
||||
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||
// GL-thread-owned side).
|
||||
Vector<XfbVarying> xfbVaryings;
|
||||
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||
Vector<String> xfbInterfaceNames;
|
||||
Vector<Uint32> xfbStrides;
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
Uint32 xfbPackedStride = 0;
|
||||
};
|
||||
|
||||
// ---- everything phase B of a link produces, in one movable block ----
|
||||
//
|
||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||
//
|
||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||
// kept the built-in would read garbage - and recorded here rather than
|
||||
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||
// bytes no longer declare the capability that armed it. The backends read it to
|
||||
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
};
|
||||
|
||||
// ---- the archive field tables (ARCHITECTURE.md:259): ONE table per type, serving both directions ----
|
||||
//
|
||||
// Visitor contract: v(const char* name, Field&) - Field is const when Self is const, so a
|
||||
// single table serves the serializer (const) and the deserializer (non-const); `Self`
|
||||
// deduces either. A visitor recurses into TypeFacts / ResourceReflection / XfbVarying by
|
||||
// calling VisitFields on the element it was handed; the tables never recurse themselves.
|
||||
//
|
||||
// Free constrained templates rather than members so the struct bodies above stay a verbatim
|
||||
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
|
||||
// struct changes its size, trips the assertion, and the message sends the author here.
|
||||
template <class Self, class V>
|
||||
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
v("isArray", a.isArray);
|
||||
v("isSizedArray", a.isSizedArray);
|
||||
v("isMatrix", a.isMatrix);
|
||||
v("isVector", a.isVector);
|
||||
v("isOpaque", a.isOpaque);
|
||||
v("isTexture", a.isTexture);
|
||||
v("isImage", a.isImage);
|
||||
v("isDouble", a.isDouble);
|
||||
v("isVoid", a.isVoid);
|
||||
v("isBuffer", a.isBuffer);
|
||||
v("isPatch", a.isPatch);
|
||||
v("hasIndex", a.hasIndex);
|
||||
v("hasFormat", a.hasFormat);
|
||||
v("vectorSize", a.vectorSize);
|
||||
v("matrixCols", a.matrixCols);
|
||||
v("matrixRows", a.matrixRows);
|
||||
v("layoutIndex", a.layoutIndex);
|
||||
v("layoutFormat", a.layoutFormat);
|
||||
v("layoutMatrix", a.layoutMatrix);
|
||||
v("basicType", a.basicType);
|
||||
} // 20 fields
|
||||
|
||||
template <class Self, class V>
|
||||
requires std::same_as<std::remove_const_t<Self>, ResourceReflection>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
v("name", a.name);
|
||||
v("glDefineType", a.glDefineType);
|
||||
v("offset", a.offset);
|
||||
v("size", a.size);
|
||||
v("index", a.index);
|
||||
v("counterIndex", a.counterIndex);
|
||||
v("arrayStride", a.arrayStride);
|
||||
v("topLevelArraySize", a.topLevelArraySize);
|
||||
v("topLevelArrayStride", a.topLevelArrayStride);
|
||||
v("binding", a.binding);
|
||||
v("location", a.location);
|
||||
v("stages", a.stages);
|
||||
v("arraySize", a.arraySize);
|
||||
v("type", a.type); // visited as a value; the visitor recurses with VisitFields(a.type, v) if it wants to
|
||||
} // 14 fields
|
||||
|
||||
template <class Self, class V>
|
||||
requires std::same_as<std::remove_const_t<Self>, XfbVarying>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
v("name", a.name);
|
||||
v("type", a.type);
|
||||
v("size", a.size);
|
||||
v("bufferIndex", a.bufferIndex);
|
||||
v("offsetBytes", a.offsetBytes);
|
||||
v("byteSize", a.byteSize);
|
||||
v("packedOffsetBytes", a.packedOffsetBytes);
|
||||
v("blockInstanceName", a.blockInstanceName);
|
||||
v("blockName", a.blockName);
|
||||
v("blockMemberIndex", a.blockMemberIndex);
|
||||
v("blockMemberElement", a.blockMemberElement);
|
||||
} // 11 fields
|
||||
|
||||
// Every member EXCEPT `program`: it is null for every archived instance by construction
|
||||
// (ProgramTranslationCache.h asserts that at insert) and must never be serialized - it is
|
||||
// the live glslang TProgram that only DoReflection may touch. 57 of the 58 members.
|
||||
template <class Self, class V>
|
||||
requires std::same_as<std::remove_const_t<Self>, LinkArtifacts>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
v("uniformReflection", a.uniformReflection);
|
||||
v("blockReflection", a.blockReflection);
|
||||
v("pipeInputReflection", a.pipeInputReflection);
|
||||
v("pipeOutputReflection", a.pipeOutputReflection);
|
||||
v("lastStageIsFragment", a.lastStageIsFragment);
|
||||
v("computeLocalSize", a.computeLocalSize);
|
||||
v("uniformIndexByName", a.uniformIndexByName);
|
||||
v("attribs", a.attribs);
|
||||
v("attribTypes", a.attribTypes);
|
||||
v("linkedFragDataLocation", a.linkedFragDataLocation);
|
||||
v("linkedFragDataIndex", a.linkedFragDataIndex);
|
||||
v("glUniformIndexToTProgram", a.glUniformIndexToTProgram);
|
||||
v("tProgramUniformIndexToGl", a.tProgramUniformIndexToGl);
|
||||
v("glBlockIndexToTProgram", a.glBlockIndexToTProgram);
|
||||
v("tProgramBlockIndexToGl", a.tProgramBlockIndexToGl);
|
||||
v("glUniformBlockIndexToBlock", a.glUniformBlockIndexToBlock);
|
||||
v("blockIndexToGlUniformBlock", a.blockIndexToGlUniformBlock);
|
||||
v("linkedExplicitUniformLocations", a.linkedExplicitUniformLocations);
|
||||
v("uniformInitialValues", a.uniformInitialValues);
|
||||
v("uniformLocations", a.uniformLocations);
|
||||
v("writtenUniformLocationBits", a.writtenUniformLocationBits);
|
||||
v("writtenUniformIndexBits", a.writtenUniformIndexBits);
|
||||
v("writtenUniformIndices", a.writtenUniformIndices);
|
||||
v("uniformIndexInTProgram", a.uniformIndexInTProgram);
|
||||
v("uniformSamplerOrImageUnitIndex", a.uniformSamplerOrImageUnitIndex);
|
||||
v("explicitOpaqueUniformBindings", a.explicitOpaqueUniformBindings);
|
||||
v("uniformBlockIndexByName", a.uniformBlockIndexByName);
|
||||
v("uniformBlockBinding", a.uniformBlockBinding);
|
||||
v("shaderStorageBlockBinding", a.shaderStorageBlockBinding);
|
||||
v("storageBlocksWithoutBinding", a.storageBlocksWithoutBinding);
|
||||
v("uniformBlocksWithoutBinding", a.uniformBlocksWithoutBinding);
|
||||
v("activeUniformCount", a.activeUniformCount);
|
||||
v("usesReservedNumSamples", a.usesReservedNumSamples);
|
||||
v("maxUniformLocation", a.maxUniformLocation);
|
||||
v("uniformNameMaxLength", a.uniformNameMaxLength);
|
||||
v("attribInNameMaxLength", a.attribInNameMaxLength);
|
||||
v("uniformBlockNameMaxLength", a.uniformBlockNameMaxLength);
|
||||
v("infoLog", a.infoLog);
|
||||
v("linkStatus", a.linkStatus);
|
||||
v("xfbVaryings", a.xfbVaryings);
|
||||
v("xfbInterfaceNames", a.xfbInterfaceNames);
|
||||
v("xfbStrides", a.xfbStrides);
|
||||
v("gsStripTriangles", a.gsStripTriangles);
|
||||
v("gsStripCaptureFixup", a.gsStripCaptureFixup);
|
||||
v("gsInputPrimitive", a.gsInputPrimitive);
|
||||
v("tcsOutputVertices", a.tcsOutputVertices);
|
||||
v("gsOutputPrimitive", a.gsOutputPrimitive);
|
||||
v("gsMaxVertices", a.gsMaxVertices);
|
||||
v("gsInvocations", a.gsInvocations);
|
||||
v("tessGenMode", a.tessGenMode);
|
||||
v("tessGenSpacing", a.tessGenSpacing);
|
||||
v("tessGenVertexOrder", a.tessGenVertexOrder);
|
||||
v("tessGenPointMode", a.tessGenPointMode);
|
||||
v("xfbBufferMode", a.xfbBufferMode);
|
||||
v("xfbVaryingNameMaxLength", a.xfbVaryingNameMaxLength);
|
||||
v("xfbNeedsScatteredCapture", a.xfbNeedsScatteredCapture);
|
||||
v("xfbPackedStride", a.xfbPackedStride);
|
||||
} // 57 fields (58 members minus `program`)
|
||||
|
||||
template <class Self, class V>
|
||||
requires std::same_as<std::remove_const_t<Self>, SpirvArtifacts>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
v("generatedSpirv", a.generatedSpirv);
|
||||
v("enableSpirvValidation", a.enableSpirvValidation);
|
||||
v("uniformOffsets", a.uniformOffsets);
|
||||
v("globalUboScratch", a.globalUboScratch);
|
||||
v("reservedNumSamplesOffset", a.reservedNumSamplesOffset);
|
||||
v("spirvStatus", a.spirvStatus);
|
||||
v("nativeFloat64", a.nativeFloat64);
|
||||
v("pointSizeDemoted", a.pointSizeDemoted);
|
||||
} // 8 fields
|
||||
|
||||
// ---- trip wires ----
|
||||
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
||||
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
|
||||
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and its serializer when one exists), then update this number");
|
||||
// The container-bearing structs have one size per standard library (std::string and
|
||||
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
|
||||
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
|
||||
// unasserted. ProgramArtifactsTest records every sizeof as a ctest property on every
|
||||
// platform, which is where a new toolchain's numbers are read from.
|
||||
#if defined(__GLIBCXX__) && !defined(_GLIBCXX_DEBUG) && (SIZE_MAX == UINT64_MAX)
|
||||
#define MGL_RESOURCEREFLECTION_SIZE 128
|
||||
#define MGL_XFBVARYING_SIZE 128
|
||||
#define MGL_LINKARTIFACTS_SIZE 1056
|
||||
#define MGL_SPIRVARTIFACTS_SIZE 88
|
||||
#elif defined(_LIBCPP_VERSION) && (SIZE_MAX == UINT64_MAX) && defined(MGL_ARTIFACT_SIZES_LIBCXX_PINNED)
|
||||
// The integrator pins these from the NDK build (brief C.4); until then this branch is inert.
|
||||
#endif
|
||||
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
|
||||
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and its serializer when one exists), then update this number");
|
||||
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
|
||||
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and its serializer when one exists), then update this number");
|
||||
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
|
||||
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and its serializer when one exists), then update this number");
|
||||
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
|
||||
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and its serializer when one exists), then update this number");
|
||||
#endif
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -9,9 +9,9 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "ShaderObject.h"
|
||||
#include "ProgramArtifacts.h"
|
||||
|
||||
#include <MG_Util/Metrics/BufferMetrics.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// The link job. Only ever held by SharedPtr here, so a forward declaration is enough -
|
||||
@@ -38,70 +38,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
// 1024 GL 4.3 requires.
|
||||
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
|
||||
|
||||
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
|
||||
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||
// AST, so a POD covers the whole surface exactly.
|
||||
struct TypeFacts {
|
||||
Bool isArray = false;
|
||||
// A runtime-sized array (a storage block's unsized trailing member) is an array
|
||||
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
|
||||
Bool isSizedArray = false;
|
||||
Bool isMatrix = false;
|
||||
Bool isVector = false;
|
||||
Bool isOpaque = false;
|
||||
Bool isTexture = false;
|
||||
Bool isImage = false;
|
||||
Bool isDouble = false; // getBasicType() == EbtDouble
|
||||
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
|
||||
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
|
||||
Bool isPatch = false; // getQualifier().patch
|
||||
Bool hasIndex = false; // getQualifier().hasIndex()
|
||||
Bool hasFormat = false; // getQualifier().hasFormat()
|
||||
Int vectorSize = 0;
|
||||
Int matrixCols = 0;
|
||||
Int matrixRows = 0;
|
||||
Int layoutIndex = 0; // getQualifier().layoutIndex
|
||||
Uint layoutFormat = 0; // getQualifier().getFormat()
|
||||
// glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against
|
||||
// the owning block's qualifier, so the getUniformBlock() fallback the old
|
||||
// accessors carried is gone.
|
||||
Int layoutMatrix = 0;
|
||||
// glslang::TBasicType, widened - ApplyUniformInitialValues and the typed
|
||||
// glGetUniform* paths compare against a handful of enumerators.
|
||||
Int basicType = 0;
|
||||
};
|
||||
|
||||
// One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
|
||||
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
|
||||
struct ResourceReflection {
|
||||
String name;
|
||||
GLenum glDefineType = 0;
|
||||
Int offset = -1;
|
||||
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
|
||||
// the resolved GL_UNIFORM_SIZE answer.
|
||||
Int size = 0;
|
||||
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
|
||||
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
|
||||
Int index = -1;
|
||||
Int counterIndex = -1;
|
||||
Int arrayStride = 0;
|
||||
Int topLevelArraySize = 0;
|
||||
Int topLevelArrayStride = 0;
|
||||
Int binding = -1;
|
||||
Int location = -1; // layoutLocation()
|
||||
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
|
||||
// nobody", which is what the dead-default-block-uniform filter tests.
|
||||
Uint32 stages = 0;
|
||||
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
|
||||
// isSizedArray()/getOuterArraySize()/size fallback.
|
||||
GLint arraySize = 1;
|
||||
TypeFacts type;
|
||||
};
|
||||
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
// The five reflection/artifact types live at namespace scope in ProgramArtifacts.h
|
||||
// (P0.5). Re-exported here so every existing spelling (ProgramObject::LinkArtifacts,
|
||||
// ProgramObject::TypeFacts, ...) compiles unchanged. Fully qualified on the right-hand
|
||||
// side on purpose: an unqualified `TypeFacts` would name the alias being declared.
|
||||
using TypeFacts = MobileGL::MG_State::GLState::TypeFacts;
|
||||
using ResourceReflection = MobileGL::MG_State::GLState::ResourceReflection;
|
||||
using UniformReflection = ResourceReflection;
|
||||
using BlockReflection = ResourceReflection;
|
||||
using PipeInputReflection = ResourceReflection;
|
||||
using PipeOutputReflection = ResourceReflection;
|
||||
using XfbVarying = MobileGL::MG_State::GLState::XfbVarying;
|
||||
using LinkArtifacts = MobileGL::MG_State::GLState::LinkArtifacts;
|
||||
using SpirvArtifacts = MobileGL::MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
|
||||
@@ -544,7 +493,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;
|
||||
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
|
||||
//
|
||||
// BOUNDS-CHECKED, and that is not defensive padding - it is the load-bearing half of
|
||||
@@ -1141,313 +1090,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
|
||||
}
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
// Offset within the gap-free record a backend that cannot express the GL
|
||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||
Uint32 packedOffsetBytes = 0;
|
||||
|
||||
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
||||
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
||||
// what the interface queries and the ESSL backend's driver-side capture list
|
||||
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
||||
// the three fields below carry what a SPIR-V backend needs instead: the
|
||||
// decoration target is the block's *instance* variable and the member index
|
||||
// inside it. blockMemberIndex < 0 means "not a block member".
|
||||
String blockInstanceName;
|
||||
String blockName;
|
||||
Int blockMemberIndex = -1;
|
||||
// Which element of an arrayed block member this capture names, -1 for "the
|
||||
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
||||
// backend needs the element index to tell a full run from a partial one.
|
||||
Int blockMemberElement = -1;
|
||||
};
|
||||
|
||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||
//
|
||||
// The membership rule is mechanical, not editorial: this is exactly the field list
|
||||
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
|
||||
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
|
||||
// as much link output). Nothing else belongs here.
|
||||
//
|
||||
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
|
||||
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
|
||||
// of thirty cross-thread field assignments. Until then this is a pure refactor.
|
||||
//
|
||||
// Access rule (invariant I5): the member below is private and reachable ONLY
|
||||
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
|
||||
// what makes "every read of link output joins the pending link" a property the
|
||||
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||
// without going through the gate.
|
||||
// ---- the owned mirror of glslang's reflection ----
|
||||
//
|
||||
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
|
||||
// asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix()
|
||||
// and friends. That made the TProgram part of the program's PERMANENT state, which
|
||||
// in turn made the whole front end (parse + link) unskippable: the L1 shader
|
||||
// translation memo could hand back the SPIR-V but the reflection still had to be
|
||||
// rebuilt from a freshly parsed AST.
|
||||
//
|
||||
// These three tables are a snapshot of everything the query surface ever reads off
|
||||
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
|
||||
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
|
||||
// they are copyable, immutable after the link, and safe to memoize and share between
|
||||
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
|
||||
// everything except DoReflection itself.
|
||||
//
|
||||
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
|
||||
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
|
||||
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
|
||||
// instead, unchanged in every other respect.
|
||||
|
||||
struct LinkArtifacts {
|
||||
// Live only between LinkProgram() and the end of DoReflection. Everything after
|
||||
// that reads the owned mirror below; a link served from the L1 memo never
|
||||
// constructs one at all, so this is null for such a program and MUST NOT be
|
||||
// dereferenced outside DoReflection.
|
||||
SharedPtr<glslang::TProgram> program;
|
||||
|
||||
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
|
||||
Vector<UniformReflection> uniformReflection;
|
||||
Vector<BlockReflection> blockReflection;
|
||||
Vector<PipeInputReflection> pipeInputReflection;
|
||||
Vector<PipeOutputReflection> pipeOutputReflection;
|
||||
// Program-level scalars glslang answers off the linked intermediates.
|
||||
// Whether the program's LAST stage is the fragment stage. A color number - and so a
|
||||
// color index - exists only there; a separable tess/geometry/vertex program's
|
||||
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||
// separate-programs-tess-control).
|
||||
Bool lastStageIsFragment = false;
|
||||
Array<GLuint, 3> computeLocalSize{};
|
||||
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||
// TProgram uniform index.
|
||||
UnorderedMap<String, Int> uniformIndexByName;
|
||||
|
||||
// Attributes (Vertex in)
|
||||
Vector<String> attribs;
|
||||
Vector<GLenum> attribTypes;
|
||||
|
||||
// FragData (Frag out): the per-link snapshot of the explicit request maps.
|
||||
UnorderedMap<String, Uint> linkedFragDataLocation;
|
||||
UnorderedMap<String, Uint> linkedFragDataIndex;
|
||||
|
||||
// GL-facing index spaces (see the translation helpers above): GL active-uniform
|
||||
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
|
||||
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
|
||||
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
|
||||
Vector<Int> glUniformIndexToTProgram;
|
||||
Vector<Int> tProgramUniformIndexToGl;
|
||||
Vector<Int> glBlockIndexToTProgram;
|
||||
Vector<Int> tProgramBlockIndexToGl;
|
||||
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
|
||||
// glBlockIndexToTProgram above.
|
||||
//
|
||||
// That list is the BLOCK space - everything the relaxed parse produced except
|
||||
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
|
||||
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
|
||||
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
|
||||
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
|
||||
// through indexToUniformBlock too, and the list therefore also carries every shader
|
||||
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
|
||||
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
|
||||
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
|
||||
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
|
||||
//
|
||||
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
|
||||
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
|
||||
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
|
||||
// DirectGLES.cpp), so compacting that list would renumber every backend binding
|
||||
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
|
||||
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||
// DoReflection assigner restores them from here).
|
||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
||||
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
|
||||
// reads until the application overwrites it, and relinking restores it - but the
|
||||
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
|
||||
// cannot carry an initializer, so the value only survives as this side-channel.
|
||||
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
|
||||
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
|
||||
UnorderedMap<String, Uint> uniformLocations;
|
||||
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
|
||||
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
|
||||
// write (GL resets uniforms to their initial values), so living here means the set
|
||||
// is cleared by the same three paths that clear the rest of a link's output -
|
||||
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
|
||||
// fourth reset site can be forgotten. Empty (and never allocated) for a program
|
||||
// that never asked to be separable.
|
||||
Vector<Uint64> writtenUniformLocationBits;
|
||||
Vector<Uint64> writtenUniformIndexBits;
|
||||
Vector<Uint> writtenUniformIndices;
|
||||
// Ordered by location,
|
||||
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||
Vector<Int> uniformIndexInTProgram;
|
||||
// ditto. Will be set at glUniform1i
|
||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||
// Sampler/image layout(binding = N) initial texture/image units, captured by
|
||||
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
|
||||
// qualifier still says what the shader declared. An OUTPUT of the link, not an
|
||||
// input to it: nothing supplies this map, the resolver fills it.
|
||||
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||
|
||||
// Ordered by uniform block index
|
||||
// index is DIFFERENT from binding!!!
|
||||
//
|
||||
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
||||
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
||||
// `prog->getUniformBlock(i) == "BlockName"`
|
||||
// These stuff are present for GL semantics, not for backend inspection
|
||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||
Vector<Int> uniformBlockBinding;
|
||||
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
//
|
||||
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
|
||||
// GL-mandated binding 0 for every storage block whose shader declared no
|
||||
// layout(binding = N). Those blocks have no other way to be told apart from a block
|
||||
// that declared one: glslang's IO mapper invents a binding and writes it into the
|
||||
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
|
||||
// default binding for this block", and a later glShaderStorageBlockBinding simply
|
||||
// overwrites it - default and rebind travel one path.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
// Block type names of the storage blocks the program's shaders declared with NO
|
||||
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||
// sees the declaration with its macros expanded.
|
||||
std::set<String> storageBlocksWithoutBinding;
|
||||
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
|
||||
// auto-mapper assigns every uniform block a binding whether or not the shader asked
|
||||
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
|
||||
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
Int uniformBlockNameMaxLength = 0;
|
||||
|
||||
String infoLog;
|
||||
Bool linkStatus = false;
|
||||
|
||||
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||
// GL-thread-owned side).
|
||||
Vector<XfbVarying> xfbVaryings;
|
||||
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||
Vector<String> xfbInterfaceNames;
|
||||
Vector<Uint32> xfbStrides;
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
Uint32 xfbPackedStride = 0;
|
||||
};
|
||||
|
||||
// ---- everything phase B of a link produces, in one movable block ----
|
||||
//
|
||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||
//
|
||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
|
||||
// the built-in there. Per PROGRAM by construction - a consumer whose producer
|
||||
// kept the built-in would read garbage - and recorded here rather than
|
||||
// re-derived because it cannot be: the rewrite's whole point is that the final
|
||||
// bytes no longer declare the capability that armed it. The backends read it to
|
||||
// respell a "gl_PointSize" transform-feedback capture as the carrier
|
||||
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
||||
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "RenderState.h"
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include "MG_Util/Debug/Log.h"
|
||||
#include "MG_Util/Types.h"
|
||||
|
||||
|
||||
@@ -8,367 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
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, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> 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, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> 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;
|
||||
};
|
||||
|
||||
namespace MG_State {
|
||||
namespace GLState {
|
||||
class RenderState {
|
||||
|
||||
@@ -8,93 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
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 {
|
||||
namespace GLState {
|
||||
class SamplerObject {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_Util/Miscellany/IndexGenerator.h>
|
||||
#include "MG_State/GLState/TextureState/TextureObject.h"
|
||||
#include "MG_Util/Types.h"
|
||||
@@ -75,7 +76,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Units above it have provably-empty binding slots, so per-draw backend scans
|
||||
// can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units.
|
||||
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
|
||||
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) {
|
||||
m_maxTouchedUnit = unit;
|
||||
// Push-on-mutation (MG_Pipe/PipeMutation.h): the high-water mark is a pushed
|
||||
// PipeInputs field, and a bind reached from inside a verb - a backend binding
|
||||
// its own synthesised fallback texture - would otherwise leave the block
|
||||
// describing a smaller scan range than the live context has.
|
||||
MGP_NOTE_MUTATION(GetMaxTouchedTextureUnit);
|
||||
}
|
||||
// Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit /
|
||||
// glBindTextures / glBindSampler) routes through here, so bumping the generation here
|
||||
// - plus in MarkTextureObjectForDeletion for delete-unbind - covers every change to
|
||||
@@ -85,11 +93,23 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Re-binding the object a slot already holds changes nothing that the generation
|
||||
// guards; such callers pass bindingChanged=false so only the high-water mark advances
|
||||
// and the backend fast path survives the redundant re-binds apps issue every frame.
|
||||
if (bindingChanged) ++m_textureBindGeneration;
|
||||
if (bindingChanged) {
|
||||
++m_textureBindGeneration;
|
||||
MGP_NOTE_MUTATION(GetTextureBindGeneration);
|
||||
}
|
||||
}
|
||||
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
|
||||
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
|
||||
void BumpTextureBindGeneration() { ++m_textureBindGeneration; }
|
||||
// Both counters below are pushed PipeInputs fields AND are moved by writes the
|
||||
// backends make into frontend objects during their own verb - a synthesised fallback
|
||||
// texture's AllocateStorage/SetInternalFormat, a sampler override's SetMinFilter, a
|
||||
// default texture becoming defined. Every such path funnels through these two
|
||||
// methods (and the bind branch above), so noticing here covers the whole family
|
||||
// rather than each writer (P1 lane finding F2; MG_Pipe/PipeMutation.h).
|
||||
void BumpTextureBindGeneration() {
|
||||
++m_textureBindGeneration;
|
||||
MGP_NOTE_MUTATION(GetTextureBindGeneration);
|
||||
}
|
||||
|
||||
// Sibling of the bind generation for everything that changes WHICH native texture a
|
||||
// backend ends up putting on a unit WITHOUT any binding moving. Two families feed it:
|
||||
@@ -108,7 +128,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// its sampled-set memo carries THIS generation alongside the bind one. Any memo of a
|
||||
// resolved per-unit binding - or of which textures a draw samples at all - needs both.
|
||||
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
|
||||
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
|
||||
void BumpSamplingResolutionGeneration() {
|
||||
++m_samplingResolutionGeneration;
|
||||
MGP_NOTE_MUTATION(GetSamplingResolutionGeneration);
|
||||
}
|
||||
|
||||
// Globally-unique, never-reused id of THIS texture state, i.e. of the context that owns
|
||||
// it. Both generations above restart at 0 with a new context, so a backend memo keyed on
|
||||
|
||||
@@ -10,65 +10,11 @@
|
||||
#include <Includes.h>
|
||||
#include "../BufferState/BufferObject.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
namespace GLState {
|
||||
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;
|
||||
};
|
||||
|
||||
class VertexArrayObject {
|
||||
public:
|
||||
// Storage capacity, not the GL-visible limit. GL_MAX_VERTEX_ATTRIBS is reported as
|
||||
|
||||
@@ -88,6 +88,9 @@ add_subdirectory(Pipeline)
|
||||
# The MGPipe catalogue arithmetic: no GL context and no driver, just the .def, the seven
|
||||
# generated files and the payload layouts.
|
||||
add_subdirectory(Pipe)
|
||||
# The P0.5 interface-purity gate: a python walk of #include lines, so it registers with no
|
||||
# MobileGL build, no GL context and no submodules (scripts/check_include_closure.py).
|
||||
add_subdirectory(Purity)
|
||||
add_subdirectory(ShaderTranspiler)
|
||||
add_subdirectory(Util)
|
||||
add_subdirectory(SelfTest)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Test/ScopedPipeVerb.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -1229,6 +1230,11 @@ TEST_F(FramebufferTest, DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWrite
|
||||
|
||||
// What the application asked for: write every channel of every draw buffer.
|
||||
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// SyncRenderState is reached from a verb, never on its own: a test that calls it directly
|
||||
// has to say which verb it stands in, or the block it reads is unfilled and unstamped and
|
||||
// its first read is Fatal{UnmigratedPipeInput} in a push build. forColorClear=false is the
|
||||
// draw arm.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
// What the driver was told. Draw buffer 0 is untouched; draw buffer 1 loses alpha.
|
||||
@@ -1260,14 +1266,22 @@ TEST_F(FramebufferTest, ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitu
|
||||
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// A draw first, so the mask really is doctored when the clear arrives...
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
{
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
}
|
||||
ASSERT_EQ(g_driverIndexedColorMasks[1].a, GL_FALSE);
|
||||
|
||||
// ...and now the clear, with NOTHING changed in the frontend parameter block. The frontend's
|
||||
// render-state version has not moved, so only the purpose-aware memo can force this push -
|
||||
// without it the clear would inherit the draw's alpha-off mask and never write the 1.0.
|
||||
ResetRecordedColorMasks();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true);
|
||||
{
|
||||
// A different verb CLASS, so a scope of its own: kClear's fill set is what a clear may
|
||||
// read, and this half has to go through on that set alone.
|
||||
MG_Test::ScopedPipeVerb clear(MG_Pipe::MGPipeVerb::Clear);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true);
|
||||
}
|
||||
ASSERT_TRUE(g_driverIndexedColorMasks[1].seen) << "the clear must re-push the colour mask";
|
||||
EXPECT_EQ(g_driverIndexedColorMasks[1].a, GL_TRUE) << "a clear is what puts the 1.0 in the stored alpha";
|
||||
|
||||
@@ -1305,6 +1319,7 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
|
||||
MG_Impl::GLImpl::ColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
|
||||
MG_Impl::GLImpl::ColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
MG_Impl::GLImpl::ColorMaski(2, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
EXPECT_EQ(g_driverIndexedColorMasks[0].a, GL_FALSE) << "the application's own alpha mask survives";
|
||||
@@ -1337,6 +1352,7 @@ TEST_F(FramebufferTest, DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsTh
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_SRC1_* is core since 3.3; glBlendFunc must take it";
|
||||
ResetRecordedBlend();
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
@@ -1357,6 +1373,7 @@ TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensio
|
||||
ResetRecordedBlend();
|
||||
|
||||
// The whole point: this used to be `throw std::runtime_error` straight through the GL ABI.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].enableSeen) << "the blend enable still has to be pushed";
|
||||
@@ -1374,6 +1391,7 @@ TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensio
|
||||
// is what has to push it.
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
draw.Renew();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled);
|
||||
@@ -1400,6 +1418,7 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
ResetRecordedBlend();
|
||||
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
|
||||
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
@@ -1416,7 +1435,10 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
|
||||
// the flag only steers the alpha-widen colour mask, so it must not reopen this either.
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
ResetRecordedBlend();
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true));
|
||||
{
|
||||
MG_Test::ScopedPipeVerb clear(MG_Pipe::MGPipeVerb::Clear);
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true));
|
||||
}
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR)) << "draw buffer " << i;
|
||||
@@ -1427,6 +1449,7 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
draw.Renew();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled) << "the enable has to be pushed - the shadow said 'off' because it was";
|
||||
@@ -1444,6 +1467,7 @@ TEST_F(FramebufferTest, DualSourceFactorsWithBlendingDisabledStillReachACapableD
|
||||
MG_Impl::GLImpl::Disable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
|
||||
@@ -24,5 +24,35 @@ if (MSVC)
|
||||
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
|
||||
endif()
|
||||
|
||||
# The P1 poison and verify shapes at the block level: a fake GLContext, the real filler and
|
||||
# accessors out of the static library. Links gtest (not gtest_main): the suite needs its own
|
||||
# main() to point MOBILEGL_LOG_FILE_PATH at a temp file before anything logs, because its
|
||||
# abort cases read the Fatal line back out of that file. Every case is a visible SKIP in a
|
||||
# pull build.
|
||||
add_executable(
|
||||
PipeInputsTest
|
||||
PipeInputsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipeInputsTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
${MGL_ROOT}/MobileGL/MG_Pipe
|
||||
${MGL_ROOT}/3rdparty/xxHash
|
||||
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
PipeInputsTest PRIVATE
|
||||
GTest::gtest
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
target_compile_options(PipeInputsTest PRIVATE /Zc:preprocessor)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
@@ -113,6 +114,39 @@ TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
|
||||
EXPECT_GE(sizeof(ResidualValueBlock), sizeof(RenderStateParameters) + sizeof(PixelStoreParameters));
|
||||
}
|
||||
|
||||
// P0.5 moved the value structs into MG_Pipe/MGPipeValueTypes.h. These are the runtime twins
|
||||
// of that header's static assertions, so the numbers show up in ctest output on every
|
||||
// platform - including one where a static assertion is skipped. Every number here is also
|
||||
// what MGL_RESIDUAL_BLOCK_SIZE (MGPipeTypes.h) and the Espryt offsetof spans depend on.
|
||||
TEST(PipeCatalogue, ValueTypeLayoutsArePinned) {
|
||||
EXPECT_EQ(sizeof(PixelStoreParameters), 28u);
|
||||
EXPECT_EQ(sizeof(PerBufferBlendState), 28u);
|
||||
EXPECT_EQ(sizeof(StencilFaceState), 28u);
|
||||
EXPECT_EQ(sizeof(RenderStateParameters), 1168u);
|
||||
EXPECT_EQ(sizeof(SamplerParameters), 100u);
|
||||
EXPECT_EQ(sizeof(MG_State::GLState::VertexAttributeVersion), 6u);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<PixelStoreParameters>);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<PerBufferBlendState>);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<StencilFaceState>);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<RenderStateParameters>);
|
||||
EXPECT_TRUE(std::is_standard_layout_v<RenderStateParameters>);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<SamplerParameters>);
|
||||
EXPECT_TRUE(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion>);
|
||||
EXPECT_LT(offsetof(RenderStateParameters, BlendStates), offsetof(RenderStateParameters, LogicOp));
|
||||
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)>, static_cast<SizeT>(kMGMaxDrawBuffers));
|
||||
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::ColorMasks)>, static_cast<SizeT>(kMGMaxDrawBuffers));
|
||||
EXPECT_EQ(kMGMaxDrawBuffers, 8u);
|
||||
}
|
||||
|
||||
// The move did not alter the carrier: the residual block is still the render-state struct,
|
||||
// then the pack struct, then the 8-aligned capability word, at the offsets it had before.
|
||||
TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
|
||||
EXPECT_EQ(offsetof(ResidualValueBlock, RenderState), 0u);
|
||||
EXPECT_EQ(offsetof(ResidualValueBlock, Pack), sizeof(RenderStateParameters));
|
||||
EXPECT_EQ(offsetof(ResidualValueBlock, CapabilityBits), 1200u);
|
||||
EXPECT_EQ(offsetof(ResidualValueBlock, PatchVertices), 1208u);
|
||||
}
|
||||
|
||||
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
|
||||
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
|
||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
|
||||
@@ -189,8 +223,14 @@ TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
|
||||
// G5's field ids come from the same accessor list as the coverage table, and every field
|
||||
// starts un-filled: reading one before its verb fills it is the poison's whole job.
|
||||
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
|
||||
EXPECT_EQ(kMGPipeInputFieldCount, 61u);
|
||||
EXPECT_EQ(kMGPipeInputFieldCount, 63u);
|
||||
MGPipeFilledState state{};
|
||||
// Before the first fill the serial is 0 as well: 0 == 0 must not read as fresh, on the
|
||||
// sticky branch either (the window D6 names "<Field>@<none>").
|
||||
EXPECT_EQ(state.CurrentVerbSerial, 0u);
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, static_cast<MGPipeInputField>(f))) << kMGPipeInputFieldNames[f];
|
||||
}
|
||||
state.CurrentVerbSerial = 1;
|
||||
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
||||
state.FilledGen[static_cast<SizeT>(MGPipeInputField::GetRenderStateParameters)] = 1;
|
||||
@@ -200,6 +240,116 @@ TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
|
||||
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
||||
}
|
||||
|
||||
// G5b: the verb enum is GLFunctionsTable's member list (69 entries), every class has verbs,
|
||||
// and the seven sticky fields ride in every class mask (P1 brief D7).
|
||||
TEST(PipeCatalogue, VerbTableIsTheFunctionTable) {
|
||||
EXPECT_EQ(kMGPipeVerbCount, 69u);
|
||||
EXPECT_EQ(kMGPipeVerbClassCount, 9u);
|
||||
SizeT perClass[kMGPipeVerbClassCount] = {};
|
||||
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
||||
++perClass[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
||||
}
|
||||
for (SizeT c = 0; c < kMGPipeVerbClassCount; ++c) {
|
||||
EXPECT_GT(perClass[c], 0u) << kMGPipeVerbClassNames[c];
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
if (kMGPipeInputFieldSticky[f]) {
|
||||
EXPECT_TRUE(MGPipeFieldMaskHas(kMGPipeClassFieldMask[c], static_cast<MGPipeInputField>(f)))
|
||||
<< kMGPipeInputFieldNames[f] << " in " << kMGPipeVerbClassNames[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
// The class table of D7, spot-checked at its edges: a draw reads the render state, a
|
||||
// query reads only the paused-primitive counter, and GenerateMipmap is a texture op.
|
||||
const auto& draw = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
||||
const auto& query = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kQuery)];
|
||||
EXPECT_TRUE(MGPipeFieldMaskHas(draw, MGPipeInputField::GetRenderStateParameters));
|
||||
EXPECT_FALSE(MGPipeFieldMaskHas(query, MGPipeInputField::GetRenderStateParameters));
|
||||
EXPECT_TRUE(MGPipeFieldMaskHas(query, MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter));
|
||||
EXPECT_EQ(kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)], MGPipeVerbClass::kTextureOp);
|
||||
EXPECT_STREQ(kMGPipeVerbNames[static_cast<SizeT>(MGPipeVerb::GetGpuTimestampNs)], "GetGpuTimestampNs");
|
||||
}
|
||||
|
||||
// The sticky set is exactly the seven forwarded, argument-keyed accessors (P1 brief D6); no
|
||||
// version or generation accessor is among them.
|
||||
TEST(PipeCatalogue, StickyFieldsAreExactlyTheSeven) {
|
||||
const char* const expected[] = {"GetBufferBindingPointCount", "GetProgramObject", "GetTextureObject",
|
||||
"HasOpenTransformFeedbackSpan", "InvalidateCompileEnv", "ValidateProgramName",
|
||||
"RecordError"};
|
||||
SizeT count = 0;
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
Bool listed = false;
|
||||
for (const char* name : expected) {
|
||||
if (std::strcmp(kMGPipeInputFieldNames[f], name) == 0) listed = true;
|
||||
}
|
||||
EXPECT_EQ(kMGPipeInputFieldSticky[f], listed) << kMGPipeInputFieldNames[f];
|
||||
if (kMGPipeInputFieldSticky[f]) ++count;
|
||||
}
|
||||
EXPECT_EQ(count, 7u);
|
||||
EXPECT_EQ(kMGPipeInputStickyFieldCount, 7u);
|
||||
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetTextureContextId)]);
|
||||
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetSamplingResolutionGeneration)]);
|
||||
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetPipelineStateVersion)]);
|
||||
}
|
||||
|
||||
// G4 compares floating point BY BITS (P1 brief D8): a NaN equals itself, a negative zero
|
||||
// does not equal a positive one, and a vector type inside an Array inside a value struct is
|
||||
// reached field by field - the differing member of the residual block is named.
|
||||
TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
||||
const Float nan = std::numeric_limits<Float>::quiet_NaN();
|
||||
const FloatVec4 a{nan, 1.f, 2.f, 3.f};
|
||||
const FloatVec4 b{nan, 1.f, 2.f, 3.f};
|
||||
EXPECT_TRUE(MGPipeFieldEqual(a, b));
|
||||
EXPECT_FALSE(a == b); // IEEE ==, the comparison the comparator must NOT use
|
||||
const FloatVec4 zero{0.f, 0.f, 0.f, 0.f};
|
||||
const FloatVec4 negativeZero{-0.f, 0.f, 0.f, 0.f};
|
||||
EXPECT_FALSE(MGPipeFieldEqual(zero, negativeZero));
|
||||
EXPECT_TRUE(zero == negativeZero);
|
||||
EXPECT_TRUE(MGPipeFieldEqual(1.5f, 1.5f));
|
||||
EXPECT_FALSE(MGPipeFieldEqual(-0.f, 0.f));
|
||||
|
||||
ResidualValueBlock left{};
|
||||
ResidualValueBlock right{};
|
||||
const char* field = nullptr;
|
||||
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
||||
right.RenderState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
|
||||
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
||||
EXPECT_STREQ(field, "RenderState");
|
||||
const char* inner = nullptr;
|
||||
EXPECT_FALSE(MGPipeVerify(left.RenderState, right.RenderState, &inner));
|
||||
EXPECT_STREQ(inner, "BlendStates");
|
||||
// A NaN patch level in the render state equals itself too.
|
||||
right = left;
|
||||
left.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
||||
right.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
||||
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
||||
}
|
||||
|
||||
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
|
||||
// the struct that used to memcmp is compared member by member.
|
||||
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 69u);
|
||||
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<StencilFaceState>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<DynamicBackendParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
||||
PixelStoreParameters p{};
|
||||
PixelStoreParameters q{};
|
||||
const char* field = nullptr;
|
||||
EXPECT_TRUE(MGPipeVerify(p, q, &field));
|
||||
q.SkipRows = 2;
|
||||
EXPECT_FALSE(MGPipeVerify(p, q, &field));
|
||||
EXPECT_STREQ(field, "SkipRows");
|
||||
MGHostSpan s{};
|
||||
MGHostSpan t{};
|
||||
t.Pad0 = 0x5A; // padding is not a field
|
||||
EXPECT_TRUE(MGPipeVerify(s, t, &field));
|
||||
t.Offset = 8;
|
||||
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
||||
EXPECT_STREQ(field, "Offset");
|
||||
}
|
||||
|
||||
// G7 pins the member list the pipeline/dynamic split is derived from.
|
||||
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
|
||||
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipe/PipeInputsTest.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 P1 poison and verify shapes at the block level (P1 brief C.1): a fake GLContext, the
|
||||
// real filler, the real accessors. Needs the push sources, so every case is a visible SKIP
|
||||
// in a pull build rather than a vanishing test. The abort cases fork (HeadlessGL.cpp's
|
||||
// pre-flight shape): the child performs the read that must be Fatal{UnmigratedPipeInput}
|
||||
// and the parent reads SIGABRT out of waitpid and the exact line out of the log file that
|
||||
// main() below points MOBILEGL_LOG_FILE_PATH at (LogLevelTest's shape). Never EXPECT_DEATH.
|
||||
//
|
||||
// The log file is per PROCESS: gtest_discover_tests runs every case as its own process, in
|
||||
// parallel under ctest -j, and a name shared between them let a sibling's unlink or Fatal
|
||||
// line land in this process's read. A forked child inherits its parent's path on purpose.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#else
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Pipe;
|
||||
|
||||
namespace {
|
||||
std::string g_logPath;
|
||||
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
long ProcessId() {
|
||||
#if defined(_WIN32)
|
||||
return static_cast<long>(::_getpid());
|
||||
#else
|
||||
return static_cast<long>(::getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// A live frontend context for the filler to read (SanityTest's idiom), restored on the
|
||||
// way out so the cases stay independent.
|
||||
class PipeInputsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_previous = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||
}
|
||||
void TearDown() override {
|
||||
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||
MG_State::pGLContext = Move(m_previous);
|
||||
}
|
||||
UniquePtr<GLContext> m_previous;
|
||||
};
|
||||
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
Bool Fresh(MGPipeInputField field) { return MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field); }
|
||||
#endif
|
||||
|
||||
[[maybe_unused]] constexpr const char* kOmittedFatal ="Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}";
|
||||
[[maybe_unused]] constexpr const char* kOmissionKnob = "GenerateMipmap:GetActiveTextureUnit";
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1; // waitpid's status; -1 when fork or waitpid failed
|
||||
std::string Log; // the lines the child appended to the log file
|
||||
};
|
||||
|
||||
// Runs `body` in a forked child and returns its wait status and log delta. The child
|
||||
// must not use gtest assertions; it _exit(0)s when `body` returns, so a body that is
|
||||
// expected to die must be asserted dead by the parent (WIFSIGNALED), never assumed.
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
const std::string before = ReadLog();
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog().substr(before.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
Bool ExitedWith(const ChildResult& r, int code) { return WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == code; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
|
||||
TEST(PipeInputsTest,OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,ReadingAnOmittedFieldAbortsNamingTheVerb) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,ReadingAFilledFieldCompletes) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,ReadingBeforeAnyFillAbortsNamingNoVerb) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,CorruptedSnapshotFieldIsNamedWithItsSerial) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,EveryVerbFillsItsClassAndNothingElse) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,MutatedFieldIsNamedAtRead) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,PoisonOmitKnobArmsTheOmission) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,BadPoisonOmitKnobIsFatalNamingTheKnob) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,VerifyCorruptKnobNamesTheFieldAtEntry) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,BadVerifyCorruptKnobIsFatalNamingTheKnob) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,VerifyFatalOffLogsTheDivergenceAndContinues) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,AFrontendMutationInsideAVerbRefreshesThePushedField) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,TheMutationNoticeRefreshesTheValueButNotTheStamp) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
TEST(PipeInputsTest,AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
|
||||
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||
}
|
||||
|
||||
#else // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// Negative control B, layer 1 (P1 brief D6 / G5): the omitted (verb, field) pair is the
|
||||
// ONLY thing that goes stale - every other bit of the verb class's mask is fresh, every
|
||||
// field outside it is not, and the verb after it neither heals the field (not in kDraw's
|
||||
// mask) nor loses one of its own.
|
||||
TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#else
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||
|
||||
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
// The value was still copied: only the stamp is withheld.
|
||||
EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::GenerateMipmap);
|
||||
// Exactly that field: a field is fresh iff its bit is in kTextureOp's mask and it is not
|
||||
// the omitted one.
|
||||
{
|
||||
const auto cls = kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)];
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(cls)];
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
const auto field = static_cast<MGPipeInputField>(f);
|
||||
const Bool expected = MGPipeFieldMaskHas(mask, field) && field != MGPipeInputField::GetActiveTextureUnit;
|
||||
EXPECT_EQ(Fresh(field), expected) << kMGPipeInputFieldNames[f] << " after the omitted GenerateMipmap fill";
|
||||
}
|
||||
}
|
||||
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetBoundVertexArray));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetRenderStateParameters));
|
||||
// A sticky field stays fresh across every verb.
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
|
||||
|
||||
// And the omission is scoped to its verb: a different verb of the same class keeps it.
|
||||
MGPipeFillForVerb(MGPipeVerb::BindImageTexture);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Negative control B, layer 2 (G5): the read itself. The child fills GenerateMipmap with the
|
||||
// omission and reads gPipeInputs.GetActiveTextureUnit(); the parent expects SIGABRT and the
|
||||
// exact Fatal line, and that nothing else was fatal.
|
||||
TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
|
||||
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
|
||||
::_exit(3); // reached only if the poison failed
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("@DrawArrays"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The sibling: the same reads without the omission complete, and no Fatal is logged.
|
||||
TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) {
|
||||
#if !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetRenderStateParameters();
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0);
|
||||
(void)gPipeInputs.GetActiveTextureUnit();
|
||||
});
|
||||
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The window before the first verb (P1 brief D6: "<Field>@<none>"). Nothing has filled this
|
||||
// process's block, so a backend-style read of a value field is Fatal naming no verb: the
|
||||
// serial and every stamp are 0, and 0 == 0 is not fresh. In a verify build the child also
|
||||
// sets the lane's knob first, the shape of a read reached from initialisation: the poison
|
||||
// fires at MGP_INPUT_CHECK, before the read hook (armed only by the first fill) could matter.
|
||||
TEST_F(PipeInputsTest, ReadingBeforeAnyFillAbortsNamingNoVerb) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
if (gPipeInputs.FilledState().CurrentVerbSerial != 0) {
|
||||
GTEST_SKIP() << "another case already filled in this process; gtest_discover_tests runs each case alone";
|
||||
}
|
||||
MG_State::pGLContext->SetLineWidth(7.0f); // a live value the default storage (0) does not hold
|
||||
const ChildResult r = RunInChild([] {
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
#endif
|
||||
(void)gPipeInputs.GetLineWidth(); // Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}
|
||||
::_exit(3); // reached only if the pre-fill window read as fresh
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetLineWidth@<none>\"}"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("verify armed"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The MOBILEGL_PIPE_POISON_OMIT parser, through the Features field the loader fills: the
|
||||
// child sets the knob after its parent filled without it, and the first fill after that
|
||||
// parses it, logs the arming line and withholds the stamp exactly as the programmatic form.
|
||||
TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
|
||||
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap"), std::string::npos)
|
||||
<< r.Log;
|
||||
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// An unknown verb in the knob is Fatal{PipeVerifyBadKnob} naming the knob and the value,
|
||||
// before any stamp is withheld.
|
||||
TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
|
||||
#if !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit\": "
|
||||
"no such verb in kMGPipeVerbNames}"),
|
||||
std::string::npos)
|
||||
<< r.Log;
|
||||
EXPECT_EQ(r.Log.find("poison omission armed"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Negative control A at the block level (G4): a clean snapshot compares equal to the pushed
|
||||
// block over the verb's mask; corrupting one field of the snapshot makes the compare name
|
||||
// exactly that field, at the serial of the fill it belongs to.
|
||||
TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#else
|
||||
const Uint64 serialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
EXPECT_EQ(serial, serialBefore + 1);
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
||||
|
||||
static PipeInputs snapshot{};
|
||||
SnapshotFromGLContext(snapshot, mask);
|
||||
MGPipeInputField field = MGPipeInputField::kFieldCount;
|
||||
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||
EXPECT_EQ(field, MGPipeInputField::kFieldCount);
|
||||
|
||||
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetRenderStateParameters));
|
||||
EXPECT_FALSE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||
EXPECT_EQ(field, MGPipeInputField::GetRenderStateParameters);
|
||||
EXPECT_STREQ(kMGPipeInputFieldNames[static_cast<SizeT>(field)], "GetRenderStateParameters");
|
||||
// The serial the report would print is the fill's, and it stamped that field.
|
||||
EXPECT_EQ(gPipeInputs.FilledState().FilledGen[static_cast<SizeT>(field)], serial);
|
||||
|
||||
// A forwarded field has nothing to corrupt, and the corruption of a field outside the
|
||||
// mask is not seen by a compare over that mask.
|
||||
EXPECT_FALSE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::RecordError));
|
||||
SnapshotFromGLContext(snapshot, mask);
|
||||
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetPixelStoreParameters));
|
||||
EXPECT_FALSE(MGPipeFieldMaskHas(mask, MGPipeInputField::GetPixelStoreParameters));
|
||||
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||
#endif
|
||||
}
|
||||
|
||||
// The compare-at-read arm (P1 brief D8, the arm that is real in P1): a value that changes in
|
||||
// the live context between the verb boundary and the read is Fatal{PipeVerifyDiffer,
|
||||
// "Field@Verb", verb=<serial>, where=read}; the same read before the mutation completes.
|
||||
TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; // the child's DrawArrays fill
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
const Float boundary = gPipeInputs.GetLineWidth(); // boundary == live: completes
|
||||
(void)gPipeInputs.GetRenderStateParameters();
|
||||
MG_State::pGLContext->SetLineWidth(boundary + 1.0f);
|
||||
if (MG_State::pGLContext->GetLineWidth() == boundary) ::_exit(7); // the mutation did not take
|
||||
(void)gPipeInputs.GetLineWidth(); // the stored value is stale against the live one: Fatal
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify read of GetLineWidth (index 0, 0) differs from the live context"),
|
||||
std::string::npos)
|
||||
<< r.Log;
|
||||
const std::string expected = "Fatal{PipeVerifyDiffer, \"GetLineWidth@DrawArrays\", verb=" + std::to_string(serial) +
|
||||
", where=read}";
|
||||
EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log;
|
||||
EXPECT_EQ(r.Log.find("where=entry"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The MOBILEGL_PIPE_VERIFY_CORRUPT parser through Features: the fill after the child arms it
|
||||
// logs both arming lines and the entry compare names the corrupted field at that fill's
|
||||
// serial, where=entry.
|
||||
TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify corruption armed - GetRenderStateParameters"), std::string::npos) << r.Log;
|
||||
const std::string expected = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" +
|
||||
std::to_string(serial) + ", where=entry}";
|
||||
EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// An unknown field name in MOBILEGL_PIPE_VERIFY_CORRUPT is Fatal{PipeVerifyBadKnob} at
|
||||
// arming, before the comparator reports anything.
|
||||
TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "NoSuchField";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField\": "
|
||||
"no such field in kMGPipeInputFieldNames}"),
|
||||
std::string::npos)
|
||||
<< r.Log;
|
||||
EXPECT_EQ(r.Log.find("verify armed"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL=0: the divergence is logged with its field and serial and the
|
||||
// process goes on (the fill returns, the next fill's compare runs again), and the teardown
|
||||
// summary counts both. The child leaves through std::exit so the comparator's static
|
||||
// destructor runs (a _exit would skip the summary line).
|
||||
TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyFatal = false;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawElements);
|
||||
std::exit(0);
|
||||
});
|
||||
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=0"), std::string::npos) << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify summary - 2 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0"),
|
||||
std::string::npos)
|
||||
<< r.Log;
|
||||
const std::string first = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" +
|
||||
std::to_string(serial) + ", where=entry}";
|
||||
const std::string second = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawElements\", verb=" +
|
||||
std::to_string(serial + 1) + ", where=entry}";
|
||||
EXPECT_NE(r.Log.find(first), std::string::npos) << "expected " << first << " in\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find(second), std::string::npos) << "expected " << second << " in\n" << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Every verb fills exactly its class mask: after a fill, a field is fresh iff its bit is set
|
||||
// (sticky fields included, since every class mask carries them).
|
||||
TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#else
|
||||
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
||||
const auto verb = static_cast<MGPipeVerb>(v);
|
||||
MGPipeFillForVerb(verb);
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
const auto field = static_cast<MGPipeInputField>(f);
|
||||
EXPECT_EQ(Fresh(field), MGPipeFieldMaskHas(mask, field))
|
||||
<< kMGPipeInputFieldNames[f] << " after " << kMGPipeVerbNames[v];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(gPipeInputs.ContextIdentity(), static_cast<const void*>(MG_State::pGLContext.get()));
|
||||
EXPECT_TRUE(gPipeInputs.IsLive());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Push on mutation (P1 lane finding F2, MG_Pipe/PipeMutation.h). The backends write into
|
||||
// frontend objects inside their own verb - Magma synthesises a fallback texture for an
|
||||
// unbound sampler and overrides a unit's sampler filter (VulkanRenderer/UniformManager) -
|
||||
// and every such write moves a counter the verb boundary already copied. The frontend
|
||||
// mutator refreshes that field, so the block still equals the live context at every read.
|
||||
// Without the notice the two reads below differ and the pushed value is the stale one.
|
||||
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbRefreshesThePushedField) {
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
ASSERT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
|
||||
ASSERT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
|
||||
ASSERT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
|
||||
|
||||
// What UniformManager's fallback path does mid-draw: change a sampler object's filter,
|
||||
// which bumps the context-wide sampling-resolution generation (SamplerObject.cpp).
|
||||
const Uint64 samplingBefore = ctx.GetSamplingResolutionGeneration();
|
||||
auto sampler = MakeShared<MG_State::GLState::SamplerObject>(0u);
|
||||
sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear
|
||||
: SamplerFilterMode::Nearest);
|
||||
ASSERT_NE(ctx.GetSamplingResolutionGeneration(), samplingBefore) << "the mutation did not move the counter";
|
||||
EXPECT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
|
||||
|
||||
// And a bind reached from inside a verb moves both the bind generation and the
|
||||
// high-water mark of touched units (TextureState::NoteUnitTouched).
|
||||
const Uint64 bindBefore = ctx.GetTextureBindGeneration();
|
||||
const Int unit = ctx.GetMaxTouchedTextureUnit() + 1;
|
||||
ctx.NoteTextureUnitTouched(unit);
|
||||
ASSERT_NE(ctx.GetTextureBindGeneration(), bindBefore) << "the bind did not move the counter";
|
||||
ASSERT_EQ(ctx.GetMaxTouchedTextureUnit(), unit);
|
||||
EXPECT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
|
||||
EXPECT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
|
||||
}
|
||||
|
||||
// The notice refreshes the VALUE and never the stamp: a field this verb withheld the stamp
|
||||
// of (negative control B) must stay stale, and a field outside the verb class's mask must
|
||||
// stay unfilled rather than be healed by an unrelated frontend write.
|
||||
TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#else
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
|
||||
ctx.BumpSamplingResolutionGeneration();
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
|
||||
<< "the notice restamped a field whose stamp the fill withheld";
|
||||
|
||||
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
|
||||
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
|
||||
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||
MGPipeFillForVerb(MGPipeVerb::FenceSync);
|
||||
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
|
||||
ctx.BumpSamplingResolutionGeneration();
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
|
||||
<< "the notice stamped a field the verb class never fills";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The lane failure itself (six DirectVulkan.Verify.UnboundImageDescriptorScenario entries,
|
||||
// two SampledSetStalenessScenario, two retrace cases): a frontend write made inside a draw
|
||||
// used to make the very next read of the pushed block differ from the live context. The
|
||||
// child reproduces it end to end under the armed comparator and must survive; without the
|
||||
// notice it dies of Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
|
||||
// where=read}.
|
||||
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
|
||||
#if !MOBILEGL_PIPE_VERIFY
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#elif !MGTEST_HAVE_FORK
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetSamplingResolutionGeneration(); // boundary == live: completes
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
const Uint64 before = ctx.GetSamplingResolutionGeneration();
|
||||
auto sampler = MakeShared<MG_State::GLState::SamplerObject>(0u);
|
||||
sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear
|
||||
: SamplerFilterMode::Nearest);
|
||||
if (ctx.GetSamplingResolutionGeneration() == before) {
|
||||
::_exit(7); // the mutation did not take: the case would pass for the wrong reason
|
||||
}
|
||||
(void)gPipeInputs.GetSamplingResolutionGeneration(); // Fatal{PipeVerifyDiffer} without the notice
|
||||
ctx.NoteTextureUnitTouched(ctx.GetMaxTouchedTextureUnit() + 1);
|
||||
(void)gPipeInputs.GetTextureBindGeneration();
|
||||
(void)gPipeInputs.GetMaxTouchedTextureUnit();
|
||||
});
|
||||
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
EXPECT_NE(r.Log.find("MGPipe: verify armed"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
|
||||
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||
// write, and caches the FILE*. The name carries this process's pid (see the file header),
|
||||
// and the file is removed on the way out; a forked child that aborts leaves it to us.
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
fs::temp_directory_path() / ("mobilegl-pipeinputs-test-" + std::to_string(ProcessId()) + ".log");
|
||||
std::error_code ec;
|
||||
fs::remove(path, ec);
|
||||
g_logPath = path.string();
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
fs::remove(path, ec);
|
||||
return rc;
|
||||
}
|
||||
@@ -275,3 +275,24 @@ gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS
|
||||
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
# Same reason again: several cases leave A links outstanding while B compiles and links.
|
||||
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
|
||||
# P0.5: the ProgramArtifacts.h header on its own - the alias identity proofs, the POD trip
|
||||
# wire and the archive field tables. Its first include is the artifacts header, so this
|
||||
# binary is also the compile-time proof that the header is self-contained.
|
||||
add_executable(
|
||||
ProgramArtifactsTest
|
||||
ProgramArtifactsTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ProgramArtifactsTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
ProgramArtifactsTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
gtest_discover_tests(ProgramArtifactsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// MobileGL - MobileGL/MG_Test/Program/ProgramArtifactsTest.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
|
||||
|
||||
// First include on purpose: the artifacts header must be self-contained (P0.5 gate A).
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#include <gtest/gtest.h>
|
||||
// For the alias checks only.
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
// P0.5 moved the five types to namespace scope and left in-class aliases behind so the
|
||||
// existing spellings compile unchanged. The classic failure of that move is a COPY: two
|
||||
// same-named definitions that both compile and silently split the type. is_same_v is the
|
||||
// compile-time proof that ProgramObject::X and GLState::X are one type.
|
||||
TEST(ProgramArtifacts, AliasesAreTheSameTypes) {
|
||||
static_assert(std::is_same_v<ProgramObject::TypeFacts, TypeFacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::ResourceReflection, ResourceReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::XfbVarying, XfbVarying>);
|
||||
static_assert(std::is_same_v<ProgramObject::LinkArtifacts, LinkArtifacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::SpirvArtifacts, SpirvArtifacts>);
|
||||
static_assert(std::is_same_v<ProgramObject::UniformReflection, UniformReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::BlockReflection, BlockReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::PipeInputReflection, PipeInputReflection>);
|
||||
static_assert(std::is_same_v<ProgramObject::PipeOutputReflection, PipeOutputReflection>);
|
||||
static_assert(ProgramObject::kInvalidUniformOffset == kInvalidUniformOffset);
|
||||
EXPECT_EQ(ProgramObject::kInvalidUniformOffset, kInvalidUniformOffset);
|
||||
EXPECT_EQ(kInvalidUniformOffset, ~0u);
|
||||
}
|
||||
|
||||
// 13 Bool + 3 bytes of padding + 7 x 4-byte scalars on every ABI.
|
||||
TEST(ProgramArtifacts, TypeFactsIsPodOf44Bytes) {
|
||||
static_assert(std::is_trivially_copyable_v<TypeFacts>);
|
||||
static_assert(std::is_standard_layout_v<TypeFacts>);
|
||||
EXPECT_EQ(sizeof(TypeFacts), 44u);
|
||||
EXPECT_EQ(alignof(TypeFacts), 4u);
|
||||
}
|
||||
|
||||
// A visitor that counts what a table hands it and checks the names are distinct - the
|
||||
// archive's one field table per type has to name every member exactly once.
|
||||
struct CountingVisitor {
|
||||
std::size_t count = 0;
|
||||
std::set<std::string> names;
|
||||
template <class Field>
|
||||
void operator()(const char* name, Field&) {
|
||||
++count;
|
||||
names.insert(name);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
std::size_t CountFields() {
|
||||
T value{};
|
||||
CountingVisitor mutableVisitor;
|
||||
VisitFields(value, mutableVisitor);
|
||||
const T& constValue = value;
|
||||
CountingVisitor constVisitor;
|
||||
VisitFields(constValue, constVisitor);
|
||||
EXPECT_EQ(mutableVisitor.count, constVisitor.count) << "const and non-const walks disagree";
|
||||
EXPECT_EQ(mutableVisitor.names.size(), mutableVisitor.count) << "a field name is listed twice";
|
||||
EXPECT_EQ(mutableVisitor.names, constVisitor.names);
|
||||
return mutableVisitor.count;
|
||||
}
|
||||
|
||||
// The field counts are the member counts of the moved structs (LinkArtifacts minus its
|
||||
// never-archived `program`). A member added without a table entry is caught by the sizeof
|
||||
// trip wires in the header; a table entry dropped without a member change is caught here.
|
||||
TEST(ProgramArtifacts, VisitFieldsCoversEveryMember) {
|
||||
EXPECT_EQ(CountFields<TypeFacts>(), 20u);
|
||||
EXPECT_EQ(CountFields<ResourceReflection>(), 14u);
|
||||
EXPECT_EQ(CountFields<XfbVarying>(), 11u);
|
||||
EXPECT_EQ(CountFields<LinkArtifacts>(), 57u);
|
||||
EXPECT_EQ(CountFields<SpirvArtifacts>(), 8u);
|
||||
}
|
||||
|
||||
// A const walk hands the visitor const references, a non-const walk mutable ones: the one
|
||||
// table really serves both directions.
|
||||
TEST(ProgramArtifacts, VisitFieldsPassesConstnessThrough) {
|
||||
LinkArtifacts artifacts;
|
||||
std::size_t mutableFields = 0;
|
||||
VisitFields(artifacts, [&](const char*, auto& field) {
|
||||
static_assert(!std::is_const_v<std::remove_reference_t<decltype(field)>>);
|
||||
++mutableFields;
|
||||
});
|
||||
const LinkArtifacts& constArtifacts = artifacts;
|
||||
std::size_t constFields = 0;
|
||||
VisitFields(constArtifacts, [&](const char*, auto& field) {
|
||||
static_assert(std::is_const_v<std::remove_reference_t<decltype(field)>>);
|
||||
++constFields;
|
||||
});
|
||||
EXPECT_EQ(mutableFields, constFields);
|
||||
// The table can write through: a deserializer's shape.
|
||||
VisitFields(artifacts, [](const char* name, auto& field) {
|
||||
if constexpr (std::is_same_v<std::remove_reference_t<decltype(field)>, Bool>) {
|
||||
if (std::string(name) == "linkStatus") field = true;
|
||||
}
|
||||
});
|
||||
EXPECT_TRUE(artifacts.linkStatus);
|
||||
}
|
||||
|
||||
// The sizeof numbers, visible in every `ctest -V` log on every platform: this is where the
|
||||
// integrator reads a new toolchain's values from before pinning them in the header.
|
||||
TEST(ProgramArtifacts, SizesArePinnedOnThisToolchain) {
|
||||
RecordProperty("sizeof_TypeFacts", static_cast<int>(sizeof(TypeFacts)));
|
||||
RecordProperty("sizeof_ResourceReflection", static_cast<int>(sizeof(ResourceReflection)));
|
||||
RecordProperty("sizeof_XfbVarying", static_cast<int>(sizeof(XfbVarying)));
|
||||
RecordProperty("sizeof_LinkArtifacts", static_cast<int>(sizeof(LinkArtifacts)));
|
||||
RecordProperty("sizeof_SpirvArtifacts", static_cast<int>(sizeof(SpirvArtifacts)));
|
||||
std::printf("sizeof: TypeFacts=%zu ResourceReflection=%zu XfbVarying=%zu LinkArtifacts=%zu SpirvArtifacts=%zu\n",
|
||||
sizeof(TypeFacts), sizeof(ResourceReflection), sizeof(XfbVarying), sizeof(LinkArtifacts),
|
||||
sizeof(SpirvArtifacts));
|
||||
#ifdef MGL_LINKARTIFACTS_SIZE // whichever STL branch the header pinned, this twin follows it
|
||||
EXPECT_EQ(sizeof(ResourceReflection), static_cast<std::size_t>(MGL_RESOURCEREFLECTION_SIZE));
|
||||
EXPECT_EQ(sizeof(XfbVarying), static_cast<std::size_t>(MGL_XFBVARYING_SIZE));
|
||||
EXPECT_EQ(sizeof(LinkArtifacts), static_cast<std::size_t>(MGL_LINKARTIFACTS_SIZE));
|
||||
EXPECT_EQ(sizeof(SpirvArtifacts), static_cast<std::size_t>(MGL_SPIRVARTIFACTS_SIZE));
|
||||
#else
|
||||
RecordProperty("sizes_pinned", "no: not the libstdc++ 64-bit toolchain");
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
# The P0.5 include-closure assertions (ROADMAP P0.5; ARCHITECTURE.md:501 gate A): no MobileGL
|
||||
# compilation, no GL context. Text mode only here, because this CTestTestfile also runs on the
|
||||
# `test` job's runner (test.yml:149-203), which has no submodules; the compiler-backed run is the
|
||||
# include-graph-check job and the developer's local `--mode both`.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if (Python3_Interpreter_FOUND)
|
||||
add_test(NAME MobileGLPurity.IncludeClosure
|
||||
COMMAND ${Python3_EXECUTABLE} ${MGL_ROOT}/scripts/check_include_closure.py --mode text --self-test --require-all)
|
||||
# ctest -L unit (test.yml:198-203). NO ENVIRONMENT property: it replaces the job env (ARCHITECTURE.md:567).
|
||||
set_tests_properties(MobileGLPurity.IncludeClosure PROPERTIES LABELS unit)
|
||||
else()
|
||||
message(STATUS "MobileGLPurity.IncludeClosure not registered: no python3 interpreter (Windows is not a correctness gate)")
|
||||
endif()
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
#include <MG_Test/ScopedPipeVerb.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
|
||||
@@ -321,7 +322,10 @@ TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
// The symptom itself: the per-unit walk has to actually bind it.
|
||||
// The symptom itself: the per-unit walk has to actually bind it. BindCurrentTextures() is
|
||||
// the per-draw walk - it reads GetProgramForDraw - so the block it reads is the one a draw
|
||||
// fills; without saying so, its first read is Fatal{UnmigratedPipeInput} in a push build.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u)
|
||||
<< "the multisample texture was not bound; every texelFetch against it reads zero";
|
||||
@@ -362,6 +366,9 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
// Each walk below is the texture half of one draw, so each gets its own verb (the second
|
||||
// and third stand after frontend state moved, exactly as a second entry point's fill would).
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u);
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
|
||||
@@ -369,6 +376,7 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
|
||||
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
|
||||
// redundant rebind while the real 2D frontend object remains current.
|
||||
draw.Renew();
|
||||
DirectGLES::BindCurrentTextures();
|
||||
EXPECT_EQ(state.bindCalls.size(), 1u);
|
||||
|
||||
@@ -379,6 +387,7 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
.GetBoundObject()
|
||||
.get()));
|
||||
|
||||
draw.Renew();
|
||||
DirectGLES::BindCurrentTextures();
|
||||
|
||||
ASSERT_EQ(state.bindCalls.size(), 2u);
|
||||
@@ -3015,7 +3024,9 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
|
||||
ASSERT_NE(resident, nullptr);
|
||||
|
||||
// First sync: builds the memo with unit 0 -> `resident`, and gives `resident`'s twin its
|
||||
// 16x16 backend storage.
|
||||
// 16x16 backend storage. The unit walk is the per-draw one, so both syncs below stand in a
|
||||
// draw - the fill is what a real glDraw* would have done before reaching this helper.
|
||||
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
|
||||
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
|
||||
auto* residentSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(resident.get());
|
||||
ASSERT_NE(residentSlot, nullptr);
|
||||
@@ -3031,6 +3042,9 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
|
||||
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(foreign);
|
||||
|
||||
const SizeT specsBeforeReplay = specs.size();
|
||||
// The second draw. Its fill re-reads the memo's keys off the live context, which is the
|
||||
// premise being tested: the silent slot swap moved none of them.
|
||||
draw.Renew();
|
||||
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
|
||||
|
||||
// `foreign` must have been synced through its OWN twin...
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// MobileGL - MobileGL/MG_Test/ScopedPipeVerb.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>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Test {
|
||||
// "This test is standing inside verb X."
|
||||
//
|
||||
// A unit test that constructs a GLContext by hand and then calls a BACKEND helper
|
||||
// directly enters through no GL entry point, so no MGP_FILL ever fires (P1 brief D7) and
|
||||
// in a push build the PipeInputs block the helper's MGB_CTX reads is empty and unstamped:
|
||||
// its first accessor read is Fatal{UnmigratedPipeInput, "Field@<none>"}. The test is
|
||||
// right and the poison is right - what was missing is the verb, and this object is how a
|
||||
// test states it. Constructing it runs the real filler for `verb`, exactly the call
|
||||
// MG_Impl makes before that verb reaches a backend; destroying it leaves the verb again.
|
||||
//
|
||||
// It is not an escape hatch and it weakens nothing:
|
||||
// - it fills exactly kMGPipeClassFieldMask[class of verb] out of the live GLContext, so
|
||||
// a read of a field that verb does not fill is still Fatal, naming the field and this
|
||||
// verb - the fill table stays the only thing that says what a verb may read;
|
||||
// - leaving the scope re-arms the poison. The exit fill is a kQuery verb, whose class
|
||||
// mask is a single field, so every field this scope stamped goes stale the moment the
|
||||
// scope ends. That matters when the suite runs as one process (a developer running
|
||||
// the test binary directly, rather than one ctest entry per case): without it, one
|
||||
// case's declaration would cover a later case that forgot to make one;
|
||||
// - it is a no-op in the pull build, where MGB_CTX is the live context and there is
|
||||
// nothing to fill, so the pull build stays byte-identical.
|
||||
//
|
||||
// Place it the way MGP_FILL is placed in production (P1 brief D7): immediately BEFORE the
|
||||
// backend call, after every frontend mutation that call is meant to see. A test that
|
||||
// drives the helper again with frontend state changed in between issues a second verb -
|
||||
// Renew() - because that is what a second GL entry point would have done. A test that
|
||||
// drives a helper of a DIFFERENT verb class opens a nested scope for it.
|
||||
class ScopedPipeVerb {
|
||||
public:
|
||||
explicit ScopedPipeVerb([[maybe_unused]] MG_Pipe::MGPipeVerb verb)
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
: m_verb(verb) {
|
||||
MG_Pipe::MGPipeFillForVerb(m_verb);
|
||||
}
|
||||
#else
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
ScopedPipeVerb(const ScopedPipeVerb&) = delete;
|
||||
ScopedPipeVerb& operator=(const ScopedPipeVerb&) = delete;
|
||||
|
||||
// A second verb of the same kind begins: re-fill and re-stamp, so a helper driven
|
||||
// again after the test moved frontend state sees the new values, the way the next GL
|
||||
// entry point's MGP_FILL would.
|
||||
void Renew() {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MG_Pipe::MGPipeFillForVerb(m_verb);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Leaving bumps the serial and puts the current verb back to "none": every field
|
||||
// this scope stamped goes stale, and a later test that forgets its own declaration
|
||||
// aborts with "<Field>@<none>" rather than with the name of a verb it never issued.
|
||||
// That matters when the suite runs as one process (a developer running the test
|
||||
// binary directly, rather than one ctest entry per case).
|
||||
~ScopedPipeVerb() { MG_Pipe::MGPipeLeaveVerb(); }
|
||||
|
||||
private:
|
||||
MG_Pipe::MGPipeVerb m_verb;
|
||||
#endif
|
||||
};
|
||||
} // namespace MobileGL::MG_Test
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_Test/ScopedPipeVerb.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -4477,6 +4478,10 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams)
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 8); // rows of 3 words (12 B) pad to 16 B
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 1);
|
||||
// The store reads the PACK block out of the frontend, and it is only ever reached from a
|
||||
// readback verb: a test that calls it directly says so, or the block is unfilled and the
|
||||
// first read is Fatal{UnmigratedPipeInput} in a push build.
|
||||
MG_Test::ScopedPipeVerb readback(MG_Pipe::MGPipeVerb::ReadPixels);
|
||||
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
|
||||
/*sliceHeight=*/2, /*sliceCount=*/1,
|
||||
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
|
||||
@@ -4499,6 +4504,8 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams)
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
|
||||
// A second readback, after the PACK parameters moved.
|
||||
readback.Renew();
|
||||
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
|
||||
/*sliceHeight=*/1, /*sliceCount=*/1,
|
||||
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
|
||||
|
||||
@@ -257,7 +257,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
|
||||
|
||||
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 client,SPIRV-Cross(`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
|
||||
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex`、`uniformBlockBinding`、`shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings`、`xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture`、`computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples`、`uniformOffsets`。`XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
|
||||
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts`、`ResourceReflection`、`XfbVarying`、`LinkArtifacts`、`SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),更新 7 个 includer,加 CI `-H` 闭包断言。同批抽取 `MG_Pipe/MGPipeValueTypes.h`(`MAX_DRAW_BUFFERS`、`PerBufferBlendState`、`StencilFaceState`、`PixelStoreParameters`、`RenderStateParameters`、`SamplerParameters`、`BorderColorForm`、`VertexAttribute`、`VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h` 与 `RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
|
||||
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts`、`ResourceReflection`、`XfbVarying`、`LinkArtifacts`、`SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),8 个 includer 靠类内 `using` 别名零改动,加 CI `-H` 闭包断言(已落地,见 `ROADMAP.md` P0.5 行;`DynamicBackendParameters` 留在 `BackendObject.h`,所以闭包门 A 断言的是 `MGPipeValueTypes.h` 而不是 `MGPipeTypes.h`)。同批抽取 `MG_Pipe/MGPipeValueTypes.h`(`MAX_DRAW_BUFFERS`、`PerBufferBlendState`、`StencilFaceState`、`PixelStoreParameters`、`RenderStateParameters`、`SamplerParameters`、`BorderColorForm`、`VertexAttribute`、`VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h` 与 `RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
|
||||
- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**,server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。
|
||||
- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw,`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。
|
||||
- Magma 的两个内部 shader(blit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 `MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。
|
||||
@@ -341,7 +341,7 @@ struct PipeInputs {
|
||||
|
||||
| 阶段 | 改什么 | 证明 |
|
||||
|---|---|---|
|
||||
| A 别名 | 机械 `sed`:`MG_State::pGLContext->` → `MGB_CTX->`(293 处)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
|
||||
| A 别名 | 机械 `sed`:`MG_State::pGLContext->` → `MGB_CTX->`(**实测 277 处**,293 出自过期的 vendored 清单)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
|
||||
| B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 |
|
||||
| C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) |
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# P0 实测
|
||||
# 实测记录(P0、P1)
|
||||
|
||||
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74C,Adreno 830,Android 16;`3B159D009VZ00000` = Oppo PLG110,Mali,Android 16(ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
|
||||
|
||||
@@ -94,3 +94,50 @@ python3 tools/trace_replay/run_android_retrace_local.py \
|
||||
3. `--env` 值里嵌入的 `/data/...` 会被 runner 的 bash.exe 做 MSYS 路径转换(`MSYS2_ARG_CONV_EXCL="/data/*"` 只覆盖开头匹配)——用 `MSYS_NO_PATHCONV=1` 跑。
|
||||
4. `coherent_as_flush` 管线完好:`--ez coherent_as_flush true` → `trace_replay_core.cpp` 的 `setenv`,独立于 `--env` 透传。
|
||||
5. **`minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败**(Adreno 830:Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`;Mali:SSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。
|
||||
|
||||
---
|
||||
|
||||
# P1 实测(`feat/disaggregated`,lavapipe / llvmpipe)
|
||||
|
||||
## 6. 规模:站点、访问器、填充点
|
||||
|
||||
| 量 | 值 | 出处 |
|
||||
|---|---|---|
|
||||
| 后端 `pGLContext->` 箭头站点 | 277(Espryt 113、Magma 164) | 计划写 293,出自过期的 vendored 清单 |
|
||||
| 非箭头行 | 58(Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT`) | 与计划一致 |
|
||||
| `PipeInputs` 字段 | 63 | 计划写 61;`GetBoundTransformFeedbackLifetimeId`、`HasOpenTransformFeedbackSpan` 是 D21 之后新增的读点 |
|
||||
| 后端调用的不同访问器 | 62(Espryt 32、Magma 56) | `GetBoundTransformFeedbackName` 已无人读,留作已标注的死行 |
|
||||
| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 | `MG_Pipe/FillPoints.def` |
|
||||
| `SyncPersistentMappedRange` / `SyncGpuWrites` | 20 / 6 | 与计划一致 |
|
||||
|
||||
## 7. verify 通道发现的两类真问题
|
||||
|
||||
**缺填充行(9 处)。** 逐 verb poison 在 79 例 retrace 与 818 条 integration-verify 上抓出三组:`kReadback` 缺 `IsTransformFeedbackActive`/`IsTransformFeedbackPaused`(深度/模板回读仿真的 `ScopedEmulationDrawState` 会暂停在飞的捕获)、`kTextureOp` 与 `kDispatch` 缺 `IsCapabilityEnabled`、`kBlitOrCopy`/`kTextureOp` 缺着色器 blit 用到的 viewport 与顶点/缓冲绑定。其中 8 行是静态过近似(代码路径可达但通道未跑到),过近似会让那对 (类, 字段) 的 poison 永久失效,**P2 收紧填充表时先复查这 8 行**。
|
||||
|
||||
**verb 内后端改前端(3 个字段)。** Magma 在自己的 draw 里写前端对象(为未绑定采样器合成回退纹理、材质化排队清除、覆写采样器 filter),把边界已经拷走的值挪了位,8 条 DirectVulkan 用例与 2 条 trace 因此报 `Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@Draw*", where=read}`。**选定的解法是 push-on-mutation**:前端计数器移动时用 `MGP_NOTE_MUTATION(Field)`(`MG_Pipe/PipeMutation.h`)刷新推送块里的那一个字段(只刷值不刷戳记),"推送块在每次读取时都等于活上下文"这条不变式因此字面成立,也正是 P2 tracker 需要的形状。
|
||||
|
||||
三个被这样处理的字段与它们的钩子:
|
||||
|
||||
| 字段 | 钩子 |
|
||||
|---|---|
|
||||
| `GetSamplingResolutionGeneration` | `TextureState::BumpSamplingResolutionGeneration()` |
|
||||
| `GetTextureBindGeneration` | `TextureState::BumpTextureBindGeneration()` 与 `NoteUnitTouched()` 的 `bindingChanged` 分支 |
|
||||
| `GetMaxTouchedTextureUnit` | `NoteUnitTouched()` 的高水位分支 |
|
||||
|
||||
钩子挂在**计数器**上而不是四十个写入点上:后端写前端的全部路径(`SamplerObject` 的 8 个 setter、`ITextureObject` 的 6 个、纹理单元绑定路径)都经由这三个计数器,`BufferObject`/`ProgramObject`/`VertexArrayObject` 的后端写入不移动任何推送字段(它们是句柄类读点)。
|
||||
|
||||
## 8. P1 验收(合入后在 `~/w7/pipe` 实测)
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| pull 构建符号与 `.text` | 0 增 / 0 删 / 0 改尺寸 / 0 重命名,`.text` 字节不变 |
|
||||
| `MG_Backend` 里的 `pGLContext` | 0(唯一保留处是开关头文件的 pull 分支) |
|
||||
| 单元(pull / push / verify) | 1485 全绿 × 3 |
|
||||
| `integration-gpu`(pull) | 878 全绿 |
|
||||
| `integration-verify` | 818 全绿,零 `Fatal{` |
|
||||
| 79 例 retrace(`MOBILEGL_PIPE_VERIFY=1`) | 79/79 通过,79/79 带 armed 摘要,零 `Fatal{` |
|
||||
| 两个阴性对照 | 4 条专用条目全绿(篡改字段变红、抽掉一个填充戳记在那条 verb 上变红) |
|
||||
| 测试名 | 0 删除,+29 |
|
||||
|
||||
**verify 构建的代价**:`integration-verify` 818 条在 4 路并行下约与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MGPipe:MobileGL 前后端拆分
|
||||
|
||||
> 状态:**P0 已落地**(`feat/disaggregated@458ccde1`,基线 `dev@81b17c0b`)。下一步 P0.5 → P1 → P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`。
|
||||
> 状态:**P0、P0.5、P1 已落地**(`feat/disaggregated`,基线 `dev@50fb1343`)。下一步 P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`。
|
||||
|
||||
## 是什么
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
| 阶段 | 天 | 落地什么 | 验收门 | 依赖 |
|
||||
|---|---|---|---|---|
|
||||
| **P0** 卫生、度量、门、骨架 | 9–11 | ✅ 边界计数器(字节 / 动态 accessor / 六个 memo 门 / 上传形状);`PipeCalls.def` 完整目录 + payload POD + 七个生成器 + CI `pipe-gates`;`gen_pipe_dirty_surface.py`;`check_doc_citations.py`;八个 `MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架(`SCM_RIGHTS` 第一优先、双 tail 双三元组的 `RingControl`、双向 doorbell、校验型 `Framing`、`ShmSegment`、`InProcessTransport`)+ `protocol.fbs` + `flatc-check` + `MG_Test/Wire` 五个套件;三个严格 no-op 收益(`GetInteger64i_v`/`GetProgramiv` 退役、`RenderbufferObject::GetLifetimeId()`、D21 XFB 计数槽重键);compute 限制进 `DynamicBackendParameters`;spike A、spike B;retrace 通道 `--env` 透传 | ✅ 单元/集成/40 trace 逐名不变;wire 层测试(fd 传递、doorbell、ring、封帧、inproc)绿;两台设备的字节/调用基线在案;spike A/B 出结论;citation lint 绿 | — |
|
||||
| **P0.5** 值头与制品头抽取 | 6–9 | `MG_Pipe/MGPipeValueTypes.h`(`RenderStateParameters`、`SamplerParameters`、`PixelStoreParameters`、`VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言 | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
|
||||
| **P1** `PipeInputs` 替换与 verify harness | 10–13 | `MG_Backend/MGPipe/PipeInputs.h`(Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
|
||||
| **P0.5** 值头与制品头抽取 | 6–9 | ✅(`5d99ee43`)`MG_Pipe/MGPipeValueTypes.h`(`RenderStateParameters`、`SamplerParameters`、`PixelStoreParameters`、`VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,8 个 includer 零改动——类内 `using` 别名保住每一种既有拼写);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言(`scripts/check_include_closure.py`,text + clang 两模式、自带阴性对照、`--require-all` 棘轮;`scripts/symbol_report.py` 做逐符号归因)。实测落地:测试名零删除、`MG_Backend` 零 diff、`.text` 字节不变、符号 0 增 / 0 删 / 42 重命名;`DynamicBackendParameters` 未搬(含 `SizeT` 与 `TextureTarget` 成员,搬动不是纯移动),`MGPipeTypes.h` 仍 include `BackendObject.h`,闭包门 A 因此断言 `MGPipeValueTypes.h` | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
|
||||
| **P1** `PipeInputs` 替换与 verify harness | 10–13 | ✅ `MG_Backend/MGPipe/PipeInputs.h`(63 字段;Espryt 32 / Magma 56 访问器);**实测 277 处箭头 + 58 行非箭头**(Espryt 113+9、Magma 164+49)逐条转换;逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
|
||||
| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | 18–26 | `MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门;`MGPipeRenderStateSpans` + G7 setter 一致性测试;`CsoCache`(64 项,键 = pipeline 子集);`create/bind_render_state` + `set_dynamic_state`(Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state`、`set_patch_state`、`set_vertex_attrib_defaults`;`set_residual_value_state` + `ResidualValueBlock` 绊线;**第一片 Track H**:Espryt 0b(`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4(`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针);`MOBILEGL_PIPE_LEGACY_MEMOS`;补 `FramebufferSrgb`/`DepthClamp` 存储 | 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照 | P1 |
|
||||
| **P3a** handle wave 1(Espryt):buffer、VAO | 18–23 | 7 个 `BufferBackendOps` → `resource_*`、`buffer_subdata_resident`(可 null)、`resource_flush_range`、`resource_readback`、`map_persistent`(不碰实现);pool 与延迟释放原样搬;vertex elements 三件(两个视图都带);`set_vertex_buffers`(`baseInstance` 显式字段);`set_index_buffer`;Adreno SIGSEGV workaround 保留 | 全套门;buffer/VAO 族场景(`LargeArenaAdoption`、`StorageBufferRegrow` 发布 `map-persistent-roundtrips`、`VertexAttribBinding`、`MultiDraw`、`PrimitiveRestart`…);Create/rd12/26.3/sodium trace;MC 26.3 在 Adreno 上 p99 不变。**再基线检查点 1:超过 27 天必须重定基线** | P2 |
|
||||
| **P4a** handle wave 2(Espryt):FBO / 纹理 / sampler / program 身份与描述符 | 26–34 | `set_framebuffer_state`(解析后的 `ReadSurface`、内联格式、`ContentHash`、`{0,1}`);sampler CSO(含 `borderColorForm`);sampler view + `set_texture_params`;`set_sampler_views`/`bind_sampler_states`/`set_shader_images`;shader CSO(SPIR-V + 归档);`set_draw/dispatch_program`;`set_global_constants`;`CompositeResolver`;纹理/renderbuffer 的 `resource_*`。emulation 在 split 下显式 Fatal 直到 P8 | 全套门;framebuffer/纹理/program 族场景;**新增"只作 attachment / image 单元 / CopyImage 端点的纹理其 `glTexParameter` 生效"场景(落地前必须红)**;两台设备 `KHR-GL46.direct_state_access.framebuffers*` 与整个 `packed_pixels` 块(~3300 例,句柄复用压力测试)。**再基线检查点 1b:超过 39 天** | P3a |
|
||||
|
||||
Executable
+696
@@ -0,0 +1,696 @@
|
||||
#!/usr/bin/env python3
|
||||
# MobileGL - scripts/check_include_closure.py
|
||||
# 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 P0.5 interface-purity gate: what the extracted headers are allowed to include.
|
||||
|
||||
ROADMAP.md P0.5 asks for a CI assertion on the include CLOSURE of the two headers the
|
||||
phase extracts, and for a negative control that goes red when an `MG_State` include is
|
||||
added back (ROADMAP.md:7 - every gate must be able to fail for the reason it exists).
|
||||
`nm --undefined-only` cannot do that job: a header that is included but whose types are
|
||||
never named leaves no symbol behind, and "included at all" is exactly the coupling P1
|
||||
and P7 have to be able to sever. So the arbiter here is the preprocessor's own
|
||||
`-H` transcript, plus a literal `#include` walk that needs no compiler at all.
|
||||
|
||||
probe header forbidden in its closure
|
||||
---------------- --------------------------------------------- -------------------------------
|
||||
value-header MG_Pipe/MGPipeValueTypes.h MG_State/ MG_Impl/ MG_Backend/ MG_Remote/
|
||||
artifacts-header MG_State/.../ProgramState/ProgramArtifacts.h ShaderObject.h, ShaderTranspiler/,
|
||||
Config.h, MG_Backend/, BufferState/,
|
||||
ProgramState/Shader* (+ <= 2 `glslang::`)
|
||||
wire-header MG_Remote/Transport/ITransport.h MobileGL/Includes.h
|
||||
|
||||
The forbidden sets deliberately say nothing about glslang, spirv-cross or vulkan:
|
||||
MobileGL/Includes.h pulls all three unconditionally (:53,:56,:59,:80-84,:130) and both
|
||||
new headers are allowed `<Includes.h>`, so a "textually glslang-free closure" assertion
|
||||
would be unsatisfiable by construction. P7 measures that with `nm -D | grep glslang` on
|
||||
the server binary instead - a symbol gate, not a text gate.
|
||||
|
||||
Two modes, and they check different things:
|
||||
|
||||
--mode text transitive walk of literal `#include` lines. No compiler, no
|
||||
submodules, blind to `#if`. This is what the ctest runs, because the
|
||||
CI `test` job's runner has no submodules checked out.
|
||||
--mode clang `-H -E` on a one-line probe TU, which is the ground truth, plus a
|
||||
`-fsyntax-only` pass that proves the header is self-contained.
|
||||
--mode both run both and additionally fail if they disagree about the violations.
|
||||
|
||||
D9 (brief section B.0): a probe whose header does not exist yet prints SKIP and is
|
||||
counted; `--require-all` turns every SKIP into a failure. That is what lets this package
|
||||
land BEFORE the two headers do, and what stops an all-SKIP run from passing for free
|
||||
once they exist - the integrator flips `--require-all` on as the ratchet.
|
||||
|
||||
python3 scripts/check_include_closure.py --mode text --self-test
|
||||
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test
|
||||
python3 scripts/check_include_closure.py --mode both --self-test --require-all
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
PREFIX = "include-closure: "
|
||||
|
||||
# The probe manifest. Header/Tu/Forbidden are the fixed contract of brief section B.0:
|
||||
# the gate is coded against these paths before the headers they name exist.
|
||||
#
|
||||
# Allow entries are deliberately awkward: each one needs a Reason that is printed in
|
||||
# the summary of every run, so that a whitelist entry has to be argued for in the pull
|
||||
# request instead of appearing quietly (test.yml:831-835 house rule).
|
||||
PROBES = [
|
||||
{
|
||||
"Name": "value-header",
|
||||
"Header": "MobileGL/MG_Pipe/MGPipeValueTypes.h",
|
||||
"Tu": "#include <MG_Pipe/MGPipeValueTypes.h>\n",
|
||||
"Forbidden": [
|
||||
"MobileGL/MG_State/",
|
||||
"MobileGL/MG_Impl/",
|
||||
"MobileGL/MG_Backend/",
|
||||
"MobileGL/MG_Remote/",
|
||||
],
|
||||
"Allow": [],
|
||||
"TextLimits": {},
|
||||
"Why": "MG_Pipe is below MG_State (ARCHITECTURE.md:550-561): the value types P1 hands "
|
||||
"the backend must not drag the frontend state tree back in.",
|
||||
},
|
||||
{
|
||||
"Name": "artifacts-header",
|
||||
"Header": "MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h",
|
||||
"Tu": "#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>\n",
|
||||
"Forbidden": [
|
||||
"MobileGL/MG_State/GLState/ProgramState/ShaderObject.h",
|
||||
"MobileGL/MG_Util/ShaderTranspiler/",
|
||||
"MobileGL/Config.h",
|
||||
"MobileGL/MG_Backend/",
|
||||
"MobileGL/MG_State/GLState/BufferState/",
|
||||
# prefix, on purpose: ShaderStage.h, ShaderCompileTask.h,
|
||||
# ShaderCompileAdoptionMap.h, ShaderPreprocessCache.h, ShaderSourceKey.h
|
||||
"MobileGL/MG_State/GLState/ProgramState/Shader",
|
||||
],
|
||||
"Allow": [],
|
||||
# D5: LinkArtifacts keeps exactly two glslang-typed members and no more.
|
||||
"TextLimits": {"glslang::": 2},
|
||||
"Why": "P7 ships the reflection artifacts over the wire; the archive header must not "
|
||||
"depend on the compiler front end that produced them.",
|
||||
},
|
||||
{
|
||||
"Name": "mutation-header",
|
||||
"Header": "MobileGL/MG_Pipe/PipeMutation.h",
|
||||
"Tu": "#include <MG_Pipe/PipeMutation.h>\n",
|
||||
"Forbidden": [
|
||||
"MobileGL/MG_Impl/",
|
||||
"MobileGL/MG_State/GLState/Core.h",
|
||||
"MobileGL/MG_Remote/",
|
||||
],
|
||||
"Allow": [],
|
||||
"TextLimits": {},
|
||||
"Why": "P1 finding F2 put MGP_NOTE_MUTATION in frontend mutators, so MG_State now "
|
||||
"includes this header (TextureState.h, SamplerObject.h). It may only DECLARE "
|
||||
"the notice: reaching MG_Impl would pull the fill implementation into the "
|
||||
"state machine that calls it, and reaching GLState/Core.h would close the "
|
||||
"cycle back onto the context. The MGPipeTypes -> BackendObject -> TextureEnum "
|
||||
"reach it inherits from MGPipe.h is D1's known exception, which is why gate A "
|
||||
"asserts MGPipeValueTypes.h rather than MGPipeTypes.h.",
|
||||
},
|
||||
{
|
||||
"Name": "wire-header",
|
||||
"Header": "MobileGL/MG_Remote/Transport/ITransport.h",
|
||||
"Tu": "#include <MG_Remote/Transport/ITransport.h>\n",
|
||||
"Forbidden": ["MobileGL/Includes.h"],
|
||||
"Allow": [],
|
||||
"TextLimits": {},
|
||||
"Why": "WireLog.h:9-24 states the rule: nothing about a byte pipe needs the GL "
|
||||
"frontend's umbrella header. Green today - it is the gate's own canary.",
|
||||
},
|
||||
]
|
||||
|
||||
# Deleting a probe must be red, not quietly green.
|
||||
REQUIRED_PROBE_NAMES = ("value-header", "artifacts-header")
|
||||
|
||||
# Measured to reproduce the closure the real build sees (666 lines for the negative
|
||||
# control TU), without a CMake configure.
|
||||
DEFAULT_CLANG_FLAGS = [
|
||||
"-std=gnu++23",
|
||||
"-Iinclude",
|
||||
"-IMobileGL",
|
||||
"-IMobileGL/MG_Pipe",
|
||||
"-I3rdparty/xxHash",
|
||||
"-I3rdparty/Vulkan-Headers/include",
|
||||
]
|
||||
|
||||
# The only submodule headers Includes.h reaches; glslang and spirv_cross are vendored.
|
||||
CLANG_MODE_PREREQS = [
|
||||
"include/ska/flat_hash_map.hpp",
|
||||
"3rdparty/xxHash/xxhash.h",
|
||||
"3rdparty/Vulkan-Headers/include/vulkan/vulkan.h",
|
||||
]
|
||||
|
||||
COMPILER_CANDIDATES = ["clang++-20", "clang++", "c++", "g++"]
|
||||
|
||||
# Text-mode angle-bracket search path, in the order MOBILEGL_INCLUDE_DIR lists it
|
||||
# (CMakeLists.txt:529-541); MG_Pipe is there twice on purpose (CMakeLists.txt:531,535).
|
||||
SEARCH_DIRS = [
|
||||
"include",
|
||||
"MobileGL",
|
||||
"MobileGL/MG_Pipe",
|
||||
"3rdparty/xxHash",
|
||||
"3rdparty/Vulkan-Headers/include",
|
||||
]
|
||||
|
||||
# `-H` writes to stderr, one line per file actually opened: dots for depth, then the path
|
||||
# as spelled on the search path. g++ appends a "Multiple include guards may be useful
|
||||
# for:" paragraph of bare paths, which must not be mistaken for depth-0 includes.
|
||||
H_LINE_RE = re.compile(r"^(\.+) (.*)$")
|
||||
|
||||
INCLUDE_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*([<"])([^>"]+)[>"]')
|
||||
|
||||
|
||||
def say(message):
|
||||
print(PREFIX + message)
|
||||
|
||||
|
||||
def error(message):
|
||||
print("::error::" + message)
|
||||
|
||||
|
||||
def repo_relative(path, cwd):
|
||||
"""normpath the -H spelling, then express it repo-relative with forward slashes.
|
||||
|
||||
Today's transcripts contain e.g. TextureState/../SamplerState/SamplerObject.h, which
|
||||
only matches a forbidden prefix after normalisation. Anything outside the repo (the
|
||||
toolchain's own headers, /usr/include) is not our business and returns None.
|
||||
"""
|
||||
absolute = os.path.normpath(os.path.join(cwd, path))
|
||||
try:
|
||||
relative = os.path.relpath(absolute, REPO_ROOT)
|
||||
except ValueError: # different drive on Windows
|
||||
return None
|
||||
if relative == os.pardir or relative.startswith(os.pardir + os.sep):
|
||||
return None
|
||||
return relative.replace(os.sep, "/")
|
||||
|
||||
|
||||
def parse_h_output(text, cwd):
|
||||
"""-H transcript -> [(depth, repo_relative_or_None, raw_spelling)] in file order."""
|
||||
entries = []
|
||||
for line in text.splitlines():
|
||||
match = H_LINE_RE.match(line)
|
||||
if not match:
|
||||
continue
|
||||
entries.append((len(match.group(1)), repo_relative(match.group(2), cwd), match.group(2)))
|
||||
return entries
|
||||
|
||||
|
||||
def resolve_include(spec, quoted, from_dir):
|
||||
candidates = []
|
||||
if quoted:
|
||||
candidates.append(os.path.join(from_dir, spec))
|
||||
for directory in SEARCH_DIRS:
|
||||
candidates.append(os.path.join(REPO_ROOT, directory, spec))
|
||||
for candidate in candidates:
|
||||
candidate = os.path.normpath(candidate)
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def text_closure(tu_text, tu_dir):
|
||||
"""Transitive walk of literal #include lines.
|
||||
|
||||
There are no macro-driven includes under MobileGL/ (3377 include lines, all literal),
|
||||
so a literal walk is exact apart from `#if`, which this mode is blind to by design -
|
||||
the compiler mode is the arbiter and --mode both cross-checks the two.
|
||||
A file is walked once, which is what an include guard does to the -H transcript too.
|
||||
"""
|
||||
entries = []
|
||||
seen = set()
|
||||
|
||||
def walk(text, from_dir, depth):
|
||||
for line in text.splitlines():
|
||||
match = INCLUDE_RE.match(line)
|
||||
if not match:
|
||||
continue
|
||||
path = resolve_include(match.group(2), match.group(1) == '"', from_dir)
|
||||
if path is None: # unresolved (libstdc++, EGL/, GL/, glslang/, ...) = leaf
|
||||
continue
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
entries.append((depth, repo_relative(path, REPO_ROOT), path))
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
||||
nested = handle.read()
|
||||
except OSError:
|
||||
continue
|
||||
walk(nested, os.path.dirname(path), depth + 1)
|
||||
|
||||
walk(tu_text, tu_dir, 1)
|
||||
return entries
|
||||
|
||||
|
||||
def find_violations(entries, forbidden, allow):
|
||||
violations = []
|
||||
for index, (depth, relative, _raw) in enumerate(entries):
|
||||
if relative is None:
|
||||
continue
|
||||
hit = next((f for f in forbidden if relative.startswith(f)), None)
|
||||
if hit is None:
|
||||
continue
|
||||
if any(relative.startswith(a["Path"]) for a in allow):
|
||||
continue
|
||||
violations.append({"index": index, "depth": depth, "path": relative, "rule": hit})
|
||||
return violations
|
||||
|
||||
|
||||
def chain_for(entries, index):
|
||||
"""Walk back to depth-1, depth-2, ... 1: the include chain that pulled the violation in."""
|
||||
chain = []
|
||||
wanted = entries[index][0]
|
||||
cursor = index
|
||||
while cursor >= 0 and wanted >= 1:
|
||||
depth, relative, raw = entries[cursor]
|
||||
if depth == wanted:
|
||||
chain.append((depth, relative or raw))
|
||||
wanted -= 1
|
||||
cursor -= 1
|
||||
return list(reversed(chain))
|
||||
|
||||
|
||||
def count_text_limits(header_path, limits):
|
||||
"""Token budgets on the header's own text (D5 pins `glslang::` at 2)."""
|
||||
problems = []
|
||||
if not limits:
|
||||
return problems
|
||||
with open(header_path, "r", encoding="utf-8", errors="replace") as handle:
|
||||
text = handle.read()
|
||||
for token, budget in sorted(limits.items()):
|
||||
found = text.count(token)
|
||||
if found > budget:
|
||||
problems.append("{} occurs {} times, budget is {}".format(token, found, budget))
|
||||
return problems
|
||||
|
||||
|
||||
def pick_compiler(explicit):
|
||||
if explicit:
|
||||
found = shutil.which(explicit) or (explicit if os.path.isfile(explicit) else None)
|
||||
if not found:
|
||||
error("compiler not found: " + explicit)
|
||||
sys.exit(1)
|
||||
return found
|
||||
env_cxx = os.environ.get("CXX")
|
||||
if env_cxx:
|
||||
found = shutil.which(env_cxx) or (env_cxx if os.path.isfile(env_cxx) else None)
|
||||
if found:
|
||||
return found
|
||||
for candidate in COMPILER_CANDIDATES:
|
||||
found = shutil.which(candidate)
|
||||
if found:
|
||||
return found
|
||||
error("no C++ preprocessor found (tried $CXX, " + ", ".join(COMPILER_CANDIDATES) + ")")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def flags_from_compile_commands(path):
|
||||
"""Any entry whose `file` is under MobileGL/ carries the flags the real build uses."""
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
database = json.load(handle)
|
||||
keep_prefixes = ("-D", "-I", "-isystem", "-std")
|
||||
for entry in database:
|
||||
source = entry.get("file", "")
|
||||
if "MobileGL/" not in source.replace(os.sep, "/"):
|
||||
continue
|
||||
tokens = entry.get("arguments")
|
||||
if tokens is None:
|
||||
tokens = shlex.split(entry.get("command", ""))
|
||||
flags = []
|
||||
skip_next = False
|
||||
keep_next = False
|
||||
for token in tokens[1:]:
|
||||
if keep_next:
|
||||
keep_next = False
|
||||
flags.append(token)
|
||||
continue
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if token in ("-o", "-c", "-MD", "-MF", "-MT", "-MMD", "-MQ"):
|
||||
skip_next = token in ("-o", "-MF", "-MT", "-MQ")
|
||||
continue
|
||||
if token == "-isystem":
|
||||
# two-token form: keep the flag AND its directory
|
||||
flags.append(token)
|
||||
keep_next = True
|
||||
continue
|
||||
if token.startswith(keep_prefixes):
|
||||
flags.append(token)
|
||||
continue
|
||||
# the input file and everything else is dropped
|
||||
return flags, entry.get("directory", REPO_ROOT)
|
||||
error("no MobileGL/ entry in " + path)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_clang_prereqs():
|
||||
missing = [p for p in CLANG_MODE_PREREQS if not os.path.isfile(os.path.join(REPO_ROOT, p))]
|
||||
if missing:
|
||||
error("clang mode needs " + ", ".join(missing) + " - run from a full checkout: "
|
||||
"git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_preprocessor(compiler, flags, tu_path, cwd):
|
||||
command = [compiler] + list(flags) + ["-H", "-E", "-o", os.devnull, tu_path]
|
||||
completed = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
return completed
|
||||
|
||||
|
||||
def run_syntax_only(compiler, flags, tu_path, cwd):
|
||||
command = [compiler] + list(flags) + ["-fsyntax-only", tu_path]
|
||||
return subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
|
||||
|
||||
def closure_for_tu(mode, tu_text, context):
|
||||
"""Return (entries, note). `context` carries compiler/flags/cwd/tmpdir."""
|
||||
if mode == "text":
|
||||
return text_closure(tu_text, REPO_ROOT), None
|
||||
tu_path = os.path.join(context["tmpdir"], "probe_%d.cpp" % context["counter"][0])
|
||||
context["counter"][0] += 1
|
||||
with open(tu_path, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(tu_text)
|
||||
completed = run_preprocessor(context["compiler"], context["flags"], tu_path, context["cwd"])
|
||||
if completed.returncode != 0:
|
||||
return parse_h_output(completed.stderr, context["cwd"]), completed.stderr.strip()
|
||||
return parse_h_output(completed.stderr, context["cwd"]), None
|
||||
|
||||
|
||||
def print_chain(entries, violation):
|
||||
for depth, path in chain_for(entries, violation["index"]):
|
||||
say(" " + (" " * (depth - 1)) + path)
|
||||
|
||||
|
||||
def run_probe(probe, mode, context, results):
|
||||
header_abs = os.path.join(REPO_ROOT, probe["Header"])
|
||||
entries, note = closure_for_tu(mode, probe["Tu"], context)
|
||||
if note:
|
||||
error("{} preprocessing failed:\n{}".format(probe["Name"], note))
|
||||
results.append({"probe": probe["Name"], "mode": mode, "status": "ERROR",
|
||||
"headers": len(entries), "forbidden": 0})
|
||||
return "ERROR", set()
|
||||
|
||||
violations = find_violations(entries, probe["Forbidden"], probe["Allow"])
|
||||
text_problems = count_text_limits(header_abs, probe["TextLimits"])
|
||||
status = "OK" if not violations and not text_problems else "FORBIDDEN"
|
||||
say("{} {} {} headers in closure, {} forbidden (mode={}, cxx={})".format(
|
||||
probe["Name"], status, len(entries), len(violations), mode,
|
||||
context["compiler_label"] if mode == "clang" else "-"))
|
||||
for allowed in probe["Allow"]:
|
||||
say(" ALLOW {} - {}".format(allowed["Path"], allowed["Reason"]))
|
||||
for violation in violations:
|
||||
say(" forbidden: {} (rule {}) reached by:".format(violation["path"], violation["rule"]))
|
||||
print_chain(entries, violation)
|
||||
for problem in text_problems:
|
||||
say(" text limit: " + problem)
|
||||
|
||||
results.append({"probe": probe["Name"], "mode": mode, "status": status,
|
||||
"headers": len(entries), "forbidden": len(violations),
|
||||
"violations": [v["path"] for v in violations],
|
||||
"text_problems": text_problems})
|
||||
return status, {v["path"] for v in violations}
|
||||
|
||||
|
||||
def run_self_contained_check(probe, context):
|
||||
"""Second assertion in clang mode: the header compiles on its own."""
|
||||
tu_path = os.path.join(context["tmpdir"], "syntax_%s.cpp" % probe["Name"].replace("-", "_"))
|
||||
with open(tu_path, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(probe["Tu"])
|
||||
completed = run_syntax_only(context["compiler"], context["flags"], tu_path, context["cwd"])
|
||||
if completed.returncode == 0:
|
||||
say("{} SELF-CONTAINED OK (-fsyntax-only, cxx={})".format(
|
||||
probe["Name"], context["compiler_label"]))
|
||||
return True
|
||||
say("{} SELF-CONTAINED FAILED (-fsyntax-only, cxx={})".format(
|
||||
probe["Name"], context["compiler_label"]))
|
||||
for line in completed.stderr.strip().splitlines()[:20]:
|
||||
say(" " + line)
|
||||
return False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# self-test: the always-on proof that the gate is capable of failing
|
||||
# --------------------------------------------------------------------------------------
|
||||
|
||||
PARSER_CANNED_GXX = """. MobileGL/MG_Pipe/MGPipeHandles.h
|
||||
.. MobileGL/Includes.h
|
||||
... MobileGL/MG_State/GLState/TextureState/../SamplerState/SamplerObject.h
|
||||
Multiple include guards may be useful for:
|
||||
/usr/include/bits/byteswap.h
|
||||
/usr/include/bits/confname.h
|
||||
"""
|
||||
|
||||
|
||||
def self_test_parser():
|
||||
"""Check 4: the trailer paragraph is discarded and a `../` spelling normalises."""
|
||||
problems = []
|
||||
entries = parse_h_output(PARSER_CANNED_GXX, REPO_ROOT)
|
||||
if len(entries) != 3:
|
||||
problems.append("g++ -H trailer not discarded: parsed {} lines, expected 3".format(len(entries)))
|
||||
depths = [e[0] for e in entries]
|
||||
if depths != [1, 2, 3]:
|
||||
problems.append("depths misparsed: {}".format(depths))
|
||||
normalised = entries[-1][1] if entries else None
|
||||
if normalised != "MobileGL/MG_State/GLState/SamplerState/SamplerObject.h":
|
||||
problems.append("`../` spelling did not normalise: {}".format(normalised))
|
||||
elif not find_violations(entries, PROBES[0]["Forbidden"], []):
|
||||
problems.append("normalised path did not match the value-header forbidden list")
|
||||
if problems:
|
||||
for problem in problems:
|
||||
say(" self-test parser: " + problem)
|
||||
return False
|
||||
say(" self-test parser: trailer discarded, `../` normalised and matched")
|
||||
return True
|
||||
|
||||
|
||||
def negative_controls():
|
||||
"""The three canned TUs of B.3, gated on which headers exist yet."""
|
||||
controls = [
|
||||
{
|
||||
"Name": "control-1 (independent of P0.5)",
|
||||
"Tu": "#include <MG_Pipe/MGPipeHandles.h>\n"
|
||||
"#include <MG_State/GLState/RenderState/RenderState.h>\n",
|
||||
"Forbidden": PROBES[0]["Forbidden"],
|
||||
"ExpectDepth1": "MobileGL/MG_State/GLState/RenderState/RenderState.h",
|
||||
},
|
||||
]
|
||||
value_header = os.path.join(REPO_ROOT, PROBES[0]["Header"])
|
||||
if os.path.isfile(value_header):
|
||||
controls.append({
|
||||
"Name": "control-2 (MG_State include added back to the value header's TU)",
|
||||
"Tu": PROBES[0]["Tu"] + "#include <MG_State/GLState/RenderState/RenderState.h>\n",
|
||||
"Forbidden": PROBES[0]["Forbidden"],
|
||||
"ExpectDepth1": "MobileGL/MG_State/GLState/RenderState/RenderState.h",
|
||||
})
|
||||
artifacts_header = os.path.join(REPO_ROOT, PROBES[1]["Header"])
|
||||
if os.path.isfile(artifacts_header):
|
||||
controls.append({
|
||||
"Name": "control-3 (ShaderObject.h added back to the artifacts header's TU)",
|
||||
"Tu": PROBES[1]["Tu"] + "#include <MG_State/GLState/ProgramState/ShaderObject.h>\n",
|
||||
"Forbidden": PROBES[1]["Forbidden"],
|
||||
"ExpectDepth1": "MobileGL/MG_State/GLState/ProgramState/ShaderObject.h",
|
||||
})
|
||||
return controls
|
||||
|
||||
|
||||
def self_test_text_limit(context):
|
||||
"""Check 3b: a third `glslang::` token in a copy of the artifacts header must trip."""
|
||||
header = os.path.join(REPO_ROOT, PROBES[1]["Header"])
|
||||
if not os.path.isfile(header):
|
||||
return None
|
||||
copy = os.path.join(context["tmpdir"], "ProgramArtifacts_with_a_third_glslang.h")
|
||||
with open(header, "r", encoding="utf-8", errors="replace") as handle:
|
||||
text = handle.read()
|
||||
with open(copy, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text + "\n// synthesized third token: glslang::TIntermediate\n")
|
||||
problems = count_text_limits(copy, PROBES[1]["TextLimits"])
|
||||
if problems:
|
||||
say(" self-test text limit: tripped as expected ({})".format(problems[0]))
|
||||
return True
|
||||
say(" self-test text limit: a third `glslang::` token did NOT trip the budget")
|
||||
return False
|
||||
|
||||
|
||||
def self_test(modes, context):
|
||||
say("self-test: negative controls (must trip) in mode(s) " + ", ".join(modes))
|
||||
ok = self_test_parser()
|
||||
trips = 0
|
||||
for control in negative_controls():
|
||||
for mode in modes:
|
||||
entries, note = closure_for_tu(mode, control["Tu"], context)
|
||||
if note:
|
||||
say(" self-test {} [{}]: preprocessing failed".format(control["Name"], mode))
|
||||
ok = False
|
||||
continue
|
||||
violations = find_violations(entries, control["Forbidden"], [])
|
||||
at_depth_1 = [v for v in violations
|
||||
if v["depth"] == 1 and v["path"] == control["ExpectDepth1"]]
|
||||
if violations and at_depth_1:
|
||||
trips += 1
|
||||
say(" self-test {} [{}]: tripped, {} violation(s), {} at depth 1".format(
|
||||
control["Name"], mode, len(violations), control["ExpectDepth1"]))
|
||||
else:
|
||||
ok = False
|
||||
say(" self-test {} [{}]: DID NOT TRIP ({} violation(s), depth-1 hit: {})".format(
|
||||
control["Name"], mode, len(violations), bool(at_depth_1)))
|
||||
limit_result = self_test_text_limit(context)
|
||||
if limit_result is False:
|
||||
ok = False
|
||||
if trips == 0:
|
||||
error("negative control did not trip: the closure gate is not checking anything")
|
||||
return False
|
||||
say("self-test: {} negative-control trip(s), parser checks {}".format(
|
||||
trips, "OK" if ok else "FAILED"))
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--mode", choices=["text", "clang", "both"], default="text",
|
||||
help="literal #include walk, the preprocessor's -H transcript, or both")
|
||||
parser.add_argument("--compiler", default=None,
|
||||
help="preprocessor to use; default $CXX, then " + ", ".join(COMPILER_CANDIDATES))
|
||||
parser.add_argument("--compile-commands", default=None,
|
||||
help="take -D/-I/-isystem/-std from a compile_commands.json entry under MobileGL/")
|
||||
parser.add_argument("--probe", action="append", default=None,
|
||||
help="restrict to this probe (repeatable)")
|
||||
parser.add_argument("--self-test", action="store_true",
|
||||
help="run the negative controls and the parser checks (always on in CI)")
|
||||
parser.add_argument("--require-all", action="store_true",
|
||||
help="a SKIP (header not present yet) is a failure")
|
||||
parser.add_argument("--json", default=None, help="write the machine-readable result here")
|
||||
args = parser.parse_args()
|
||||
|
||||
names = [p["Name"] for p in PROBES]
|
||||
for required in REQUIRED_PROBE_NAMES:
|
||||
if required not in names:
|
||||
error("probe `{}` is missing from the manifest in {}".format(
|
||||
required, os.path.basename(__file__)))
|
||||
return 1
|
||||
|
||||
modes = ["text", "clang"] if args.mode == "both" else [args.mode]
|
||||
if "clang" in modes:
|
||||
check_clang_prereqs()
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="mgl-include-closure-")
|
||||
atexit.register(shutil.rmtree, tmpdir, True)
|
||||
context = {
|
||||
"tmpdir": tmpdir,
|
||||
"counter": [0],
|
||||
"cwd": REPO_ROOT,
|
||||
"flags": list(DEFAULT_CLANG_FLAGS),
|
||||
"compiler": None,
|
||||
"compiler_label": "-",
|
||||
}
|
||||
if "clang" in modes:
|
||||
compiler = pick_compiler(args.compiler)
|
||||
context["compiler"] = compiler
|
||||
context["compiler_label"] = compiler
|
||||
if args.compile_commands:
|
||||
flags, directory = flags_from_compile_commands(args.compile_commands)
|
||||
context["flags"] = flags
|
||||
context["cwd"] = directory
|
||||
say("flags from {} ({} tokens), cwd={}".format(
|
||||
args.compile_commands, len(flags), directory))
|
||||
say("compiler: " + compiler)
|
||||
|
||||
selected = [p for p in PROBES if args.probe is None or p["Name"] in args.probe]
|
||||
if args.probe:
|
||||
unknown = sorted(set(args.probe) - set(names))
|
||||
if unknown:
|
||||
error("unknown probe(s): " + ", ".join(unknown))
|
||||
return 1
|
||||
|
||||
results = []
|
||||
problems = 0
|
||||
skipped = 0
|
||||
if args.require_all:
|
||||
# The ratchet is about the two P0.5 headers existing, whether or not --probe
|
||||
# narrowed this run to something else: a required header that is missing is a
|
||||
# failure even when its probe was not selected.
|
||||
selected_names = {probe["Name"] for probe in selected}
|
||||
for probe in PROBES:
|
||||
if probe["Name"] in REQUIRED_PROBE_NAMES and probe["Name"] not in selected_names and not os.path.isfile(os.path.join(REPO_ROOT, probe["Header"])):
|
||||
problems += 1
|
||||
say("{} is required under --require-all but its header does not exist: {}".format(
|
||||
probe["Name"], probe["Header"]))
|
||||
for probe in selected:
|
||||
# D9: a header that does not exist yet is one SKIP line, counted once, and a
|
||||
# failure only under --require-all (the ratchet the integrator flips).
|
||||
if not os.path.isfile(os.path.join(REPO_ROOT, probe["Header"])):
|
||||
say("{} SKIP header not present yet: {}".format(probe["Name"], probe["Header"]))
|
||||
results.append({"probe": probe["Name"], "mode": "+".join(modes), "status": "SKIP",
|
||||
"headers": 0, "forbidden": 0})
|
||||
skipped += 1
|
||||
if args.require_all:
|
||||
problems += 1
|
||||
say("{} SKIP is a failure under --require-all: {} does not exist".format(
|
||||
probe["Name"], probe["Header"]))
|
||||
continue
|
||||
per_mode_status = {}
|
||||
per_mode_violations = {}
|
||||
for mode in modes:
|
||||
status, violations = run_probe(probe, mode, context, results)
|
||||
per_mode_status[mode] = status
|
||||
per_mode_violations[mode] = violations
|
||||
if any(s not in ("OK",) for s in per_mode_status.values()):
|
||||
problems += 1
|
||||
if len(modes) > 1:
|
||||
sets = list(per_mode_violations.values())
|
||||
if sets[0] != sets[1]:
|
||||
problems += 1
|
||||
say("{} MODE DISAGREEMENT - text and clang do not see the same violations".format(
|
||||
probe["Name"]))
|
||||
say(" text : " + (", ".join(sorted(per_mode_violations["text"])) or "(none)"))
|
||||
say(" clang: " + (", ".join(sorted(per_mode_violations["clang"])) or "(none)"))
|
||||
if "clang" in modes:
|
||||
if not run_self_contained_check(probe, context):
|
||||
problems += 1
|
||||
|
||||
self_test_ok = True
|
||||
if args.self_test:
|
||||
self_test_ok = self_test(modes, context)
|
||||
if not self_test_ok:
|
||||
problems += 1
|
||||
|
||||
say("{} probes, {} skipped, {} problem(s)".format(len(selected), skipped, problems))
|
||||
|
||||
if args.json:
|
||||
with open(args.json, "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump({"modes": modes, "results": results, "skipped": skipped,
|
||||
"problems": problems, "self_test": self_test_ok,
|
||||
"require_all": args.require_all}, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
if problems:
|
||||
error("include-closure gate failed: {} problem(s); see the chains above".format(problems))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.setrecursionlimit(10000)
|
||||
sys.exit(main())
|
||||
+551
-34
@@ -8,11 +8,14 @@
|
||||
# End of Source File Header
|
||||
"""The seven MGPipe generators, G1..G7 (plan B section 4.1).
|
||||
|
||||
Reads the three hand-maintained sources of truth
|
||||
Reads the four hand-maintained sources of truth
|
||||
|
||||
MobileGL/MG_Pipe/PipeCalls.def the call catalogue
|
||||
MobileGL/MG_Pipe/PipeFields.def per-payload field lists for the verify comparator
|
||||
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory
|
||||
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory,
|
||||
and the sticky-field list
|
||||
MobileGL/MG_Pipe/FillPoints.def verb -> class and class -> may-read field tables
|
||||
(G5b), checked against MG_Backend::GLFunctionsTable
|
||||
|
||||
plus the vendored copy of the backend read inventory
|
||||
|
||||
@@ -22,11 +25,15 @@ and writes MobileGL/MG_Pipe/generated/*.inc. The outputs are COMMITTED; CI regen
|
||||
them and fails on a diff, which is what keeps the seven generators from drifting apart
|
||||
from the catalogue (they all consume the same .def).
|
||||
|
||||
python3 scripts/gen_pipe.py # write the generated files, print the summary
|
||||
python3 scripts/gen_pipe.py --check # fail if regenerating would change anything
|
||||
python3 scripts/gen_pipe.py # write the generated files, print the summary
|
||||
python3 scripts/gen_pipe.py --check # fail if regenerating would change anything
|
||||
python3 scripts/gen_pipe.py --self-test # the negative controls: each gate below must trip
|
||||
|
||||
Both modes refuse a catalogue whose call payload has no field list in PipeFields.def: a
|
||||
payload the G4 comparator cannot see is a payload MOBILEGL_PIPE_VERIFY is blind to.
|
||||
payload the G4 comparator cannot see is a payload MOBILEGL_PIPE_VERIFY is blind to. Both
|
||||
also refuse a field list that does not name every data member of its struct (or names one
|
||||
that is not a member), and a backend accessor read through MGB_CTX-> / pGLContext-> that
|
||||
has no Coverage.def row (P1 brief D8, D12).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -38,6 +45,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
|
||||
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
|
||||
INVENTORY = os.path.join(REPO_ROOT, "scripts", "data", "backend_read_inventory.md")
|
||||
FUNCTION_TABLE_HEADER = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend", "BackendObject.h")
|
||||
|
||||
GENERATED_BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
@@ -160,15 +168,23 @@ def parse_calls():
|
||||
return calls
|
||||
|
||||
|
||||
# The member types the G4 comparator falls back to memcmp for (see gen_verify): the
|
||||
# MG_State / MG_Backend value structs and MGHostSpan. They are not call payloads and get
|
||||
# field lists of their own in P0.5. Nothing else may be missing from PipeFields.def.
|
||||
MEMCMP_FALLBACK_TYPES = {
|
||||
"RenderStateParameters",
|
||||
"PixelStoreParameters",
|
||||
"DynamicBackendParameters",
|
||||
"MGHostSpan",
|
||||
}
|
||||
# Types the G4 comparator may fall back to memcmp for: NONE since P1. The value structs and
|
||||
# MGHostSpan have field lists of their own, and the fallback branch of MGPipeFieldEqual is a
|
||||
# static_assert, so a future struct without a field list is a compile error rather than a
|
||||
# padding false positive. Kept as a (deliberately empty) set so the check below keeps its
|
||||
# shape.
|
||||
MEMCMP_FALLBACK_TYPES = set()
|
||||
|
||||
# Where the structs named in PipeFields.def are declared: the payload header, the value
|
||||
# header, the host-span header and - for DynamicBackendParameters, the caps block - the
|
||||
# backend object header.
|
||||
FIELD_LIST_STRUCT_HEADERS = [
|
||||
os.path.join(PIPE_DIR, "MGPipeTypes.h"),
|
||||
os.path.join(PIPE_DIR, "MGPipeValueTypes.h"),
|
||||
os.path.join(PIPE_DIR, "MGPipeHostSpan.h"),
|
||||
FUNCTION_TABLE_HEADER,
|
||||
]
|
||||
BACKEND_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend")
|
||||
|
||||
|
||||
def parse_verify_payloads():
|
||||
@@ -183,6 +199,253 @@ def parse_verify_payloads():
|
||||
return payloads
|
||||
|
||||
|
||||
FIELD_LIST_RE = re.compile(r"#define MGP_FIELDS_(\w+)\(F\)")
|
||||
|
||||
|
||||
def parse_field_lists(text=None):
|
||||
"""PipeFields.def -> {payload: [field, ...]} for every MGP_FIELDS_<Payload>(F) macro; the
|
||||
macro ends at the first line without a continuation backslash."""
|
||||
if text is None:
|
||||
text = read(os.path.join(PIPE_DIR, "PipeFields.def"))
|
||||
lists = {}
|
||||
lines = text.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
match = FIELD_LIST_RE.match(lines[i])
|
||||
if not match:
|
||||
i += 1
|
||||
continue
|
||||
name = match.group(1)
|
||||
body = []
|
||||
while i < len(lines):
|
||||
body.append(lines[i])
|
||||
if not lines[i].rstrip().endswith("\\"):
|
||||
break
|
||||
i += 1
|
||||
i += 1
|
||||
fields = re.findall(r"\bF\((\w+)\)", "\n".join(body))
|
||||
if name in lists:
|
||||
sys.exit("PipeFields.def: MGP_FIELDS_%s is defined twice" % name)
|
||||
lists[name] = fields
|
||||
return lists
|
||||
|
||||
|
||||
def mask_comments_and_strings(text):
|
||||
"""Replace comment and string-literal bodies with spaces, keeping every offset and
|
||||
newline, so the regexes below cannot match inside a comment or a literal
|
||||
(gen_pipe_dirty_surface.py's shape)."""
|
||||
out = list(text)
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == "/" and i + 1 < n and text[i + 1] == "/":
|
||||
while i < n and text[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
elif c == "/" and i + 1 < n and text[i + 1] == "*":
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
|
||||
if text[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i < n:
|
||||
out[i] = " "
|
||||
if i + 1 < n:
|
||||
out[i + 1] = " "
|
||||
i += 2
|
||||
elif c in "\"'":
|
||||
quote = c
|
||||
i += 1
|
||||
while i < n and text[i] != quote:
|
||||
if text[i] == "\\":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i < n and text[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i < n:
|
||||
out[i] = " "
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
PADDING_MEMBER_RE = re.compile(r"^Pad\d*$")
|
||||
NESTED_TYPE_RE = re.compile(r"^\s*(?:struct|class|union|enum)\b")
|
||||
FUNCTION_HEAD_RE = re.compile(r"\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:=\s*(?:default|delete|0)\s*)?$")
|
||||
NON_MEMBER_RE = re.compile(r"^\s*(?:static|using|typedef|friend|template|explicit|virtual|operator)\b")
|
||||
|
||||
|
||||
def find_struct_body(masked, name):
|
||||
"""The text between the braces of `struct <name> {` (an optional base list allowed), or
|
||||
None. Comments and strings must already be masked."""
|
||||
match = re.search(r"\bstruct\s+%s\s*(?::[^{;]*)?\{" % re.escape(name), masked)
|
||||
if not match:
|
||||
return None
|
||||
i = match.end()
|
||||
depth = 1
|
||||
start = i
|
||||
while i < len(masked) and depth:
|
||||
if masked[i] == "{":
|
||||
depth += 1
|
||||
elif masked[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
return masked[start:i - 1]
|
||||
|
||||
|
||||
def matching_brace(text, open_index):
|
||||
depth = 0
|
||||
j = open_index
|
||||
while j < len(text):
|
||||
if text[j] == "{":
|
||||
depth += 1
|
||||
elif text[j] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return j
|
||||
j += 1
|
||||
return len(text) - 1
|
||||
|
||||
|
||||
def strip_balanced(text, open_char, close_char):
|
||||
out = []
|
||||
depth = 0
|
||||
for c in text:
|
||||
if c == open_char:
|
||||
depth += 1
|
||||
elif c == close_char:
|
||||
depth -= 1
|
||||
elif depth == 0:
|
||||
out.append(c)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def member_names(statement):
|
||||
"""The data-member names declared by one struct-body statement, or [] for anything
|
||||
that is not a data member (a function, a static, a using, an access label...)."""
|
||||
statement = re.sub(r"^\s*(?:public|private|protected)\s*:", "", statement).strip()
|
||||
if not statement or NON_MEMBER_RE.match(statement):
|
||||
return []
|
||||
# The declarator part is what precedes the default initializer.
|
||||
left = re.split(r"=|\{\.\.\.\}", statement, maxsplit=1)[0]
|
||||
if "(" in left:
|
||||
return [] # a function declaration
|
||||
left = strip_balanced(left, "<", ">")
|
||||
left = strip_balanced(left, "[", "]")
|
||||
names = []
|
||||
for k, chunk in enumerate(left.split(",")):
|
||||
tokens = re.findall(r"[A-Za-z_]\w*", chunk)
|
||||
if k == 0 and len(tokens) < 2:
|
||||
return [] # no type: not a declaration
|
||||
if not tokens:
|
||||
return []
|
||||
names.append(tokens[-1])
|
||||
return [n for n in names if not PADDING_MEMBER_RE.match(n)]
|
||||
|
||||
|
||||
def struct_data_members(body):
|
||||
"""Direct data members of a struct body, in declaration order: statics, member
|
||||
functions, nested types and Pad-named members excluded."""
|
||||
members = []
|
||||
statement = []
|
||||
i = 0
|
||||
n = len(body)
|
||||
while i < n:
|
||||
c = body[i]
|
||||
if c == "{":
|
||||
head = "".join(statement)
|
||||
close = matching_brace(body, i)
|
||||
if NESTED_TYPE_RE.match(head.strip()) or FUNCTION_HEAD_RE.search(head.rstrip()):
|
||||
# A nested type or a member-function body: not a data member. Swallow the
|
||||
# nested type's trailing semicolon too.
|
||||
statement = []
|
||||
i = close + 1
|
||||
if NESTED_TYPE_RE.match(head.strip()):
|
||||
while i < n and body[i] in " \t\n":
|
||||
i += 1
|
||||
if i < n and body[i] == ";":
|
||||
i += 1
|
||||
continue
|
||||
statement.append("{...}") # a brace default initializer
|
||||
i = close + 1
|
||||
continue
|
||||
if c == ";":
|
||||
members.extend(member_names("".join(statement)))
|
||||
statement = []
|
||||
i += 1
|
||||
continue
|
||||
statement.append(c)
|
||||
i += 1
|
||||
return members
|
||||
|
||||
|
||||
def check_field_lists_cover_struct_members(field_lists, payloads, header_texts=None):
|
||||
"""Every payload in MGP_VERIFY_PAYLOAD_LIST: its MGP_FIELDS_ list must name every direct
|
||||
data member of `struct <Payload> {` (Pad-named members are padding and excluded) and
|
||||
nothing that is not a member. A member without an F(...) is a field MOBILEGL_PIPE_VERIFY
|
||||
is blind to; an F(...) that is not a member is a list that stopped describing its
|
||||
struct. Runs in both modes, --check included, so it is part of pipe-gates."""
|
||||
if header_texts is None:
|
||||
header_texts = [read(path) for path in FIELD_LIST_STRUCT_HEADERS]
|
||||
masked = [mask_comments_and_strings(t) for t in header_texts]
|
||||
problems = []
|
||||
for payload in payloads:
|
||||
body = None
|
||||
for m in masked:
|
||||
body = find_struct_body(m, payload)
|
||||
if body is not None:
|
||||
break
|
||||
if body is None:
|
||||
problems.append("%s: struct not found in %s" % (payload, ", ".join(os.path.basename(p) for p in FIELD_LIST_STRUCT_HEADERS)))
|
||||
continue
|
||||
members = struct_data_members(body)
|
||||
listed = field_lists.get(payload, [])
|
||||
missing = [m for m in members if m not in listed]
|
||||
extra = [f for f in listed if f not in members]
|
||||
if missing:
|
||||
problems.append("%s: member(s) with no F(...) in PipeFields.def: %s" % (payload, ", ".join(missing)))
|
||||
if extra:
|
||||
problems.append("%s: F(...) name(s) that are not members: %s" % (payload, ", ".join(extra)))
|
||||
if not members:
|
||||
problems.append("%s: no data members parsed" % payload)
|
||||
if problems:
|
||||
sys.exit("PipeFields.def does not cover its structs:\n " + "\n ".join(problems))
|
||||
|
||||
|
||||
ACCESSOR_READ_RE = re.compile(r"\b(?:MGB_CTX|pGLContext)\s*->\s*(\w+)")
|
||||
|
||||
|
||||
def scan_live_accessors(accessors, backend_dir=None, verbose=True):
|
||||
"""Every accessor a backend reads through MGB_CTX-> or pGLContext-> (comments and
|
||||
strings masked) must have a Coverage.def row - a read without a row is a PipeInputs
|
||||
field that does not exist. Rows no backend reads are printed, not refused (the dead
|
||||
GetBoundTransformFeedbackName row is deliberate). Returns the set of names read."""
|
||||
if backend_dir is None:
|
||||
backend_dir = BACKEND_DIR
|
||||
known = set(name for name, _ in accessors)
|
||||
read_names = {}
|
||||
for root, _, files in os.walk(backend_dir):
|
||||
for name in sorted(files):
|
||||
if not name.endswith((".cpp", ".h")):
|
||||
continue
|
||||
path = os.path.join(root, name)
|
||||
masked = mask_comments_and_strings(read(path))
|
||||
for match in ACCESSOR_READ_RE.finditer(masked):
|
||||
read_names.setdefault(match.group(1), set()).add(os.path.relpath(path, REPO_ROOT))
|
||||
unknown = sorted(n for n in read_names if n not in known)
|
||||
if unknown:
|
||||
sys.exit("Coverage.def: accessor(s) read by a backend with no row: %s"
|
||||
% ", ".join("%s (%s)" % (n, ", ".join(sorted(read_names[n]))) for n in unknown))
|
||||
unread = sorted(known - set(read_names))
|
||||
if verbose and unread:
|
||||
print("gen_pipe: %d accessor row(s) no backend reads: %s" % (len(unread), ", ".join(unread)))
|
||||
return set(read_names)
|
||||
|
||||
|
||||
def check_call_payloads_have_field_lists(calls, payloads):
|
||||
"""Every payload PipeCalls.def names must have a G4 field list, or the verify comparator
|
||||
is silently blind to that call. Runs in both modes, --check included."""
|
||||
@@ -208,7 +471,18 @@ def parse_coverage():
|
||||
sys.exit("Coverage.def: MGP_COVERAGE_DELTA_LIST is missing")
|
||||
for kind, call in re.findall(r"X\(([^,]+),\s*(\w+)\)", block.group(1)):
|
||||
deltas.append((kind.strip(), call))
|
||||
return accessors, deltas
|
||||
sticky = []
|
||||
block = re.search(r"#define MGP_COVERAGE_STICKY_LIST\(X\)(.*?)\n\n", text, re.S)
|
||||
if not block:
|
||||
sys.exit("Coverage.def: MGP_COVERAGE_STICKY_LIST is missing")
|
||||
accessor_names = set(name for name, _ in accessors)
|
||||
for name, reason in re.findall(r"X\((\w+)\s*,\s*\"([^\"]*)\"\)", block.group(1)):
|
||||
if name not in accessor_names:
|
||||
sys.exit("Coverage.def: sticky field %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST" % name)
|
||||
if name in dict(sticky):
|
||||
sys.exit("Coverage.def: sticky field %s is listed twice" % name)
|
||||
sticky.append((name, reason))
|
||||
return accessors, deltas, sticky
|
||||
|
||||
|
||||
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
|
||||
@@ -387,6 +661,23 @@ def gen_verify(payloads):
|
||||
""")
|
||||
out.append("template <class T>")
|
||||
out.append("struct MGPipeHasFieldVerifier : std::false_type {};\n")
|
||||
out.append("""// 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]);
|
||||
""")
|
||||
for payload in payloads:
|
||||
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField);"
|
||||
% (payload, payload))
|
||||
@@ -400,6 +691,8 @@ 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>) {
|
||||
@@ -407,15 +700,23 @@ inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
||||
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
|
||||
return a == b;
|
||||
} else {
|
||||
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
|
||||
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
|
||||
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
|
||||
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
|
||||
// and this branch stops being reachable from any payload.
|
||||
return std::memcmp(&a, &b, sizeof(T)) == 0;
|
||||
// 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) {
|
||||
@@ -443,8 +744,9 @@ inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def gen_filled(accessors, calls):
|
||||
def gen_filled(accessors, calls, sticky):
|
||||
call_names = set(c.Name for c in calls)
|
||||
sticky_map = dict(sticky)
|
||||
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
|
||||
"Coverage.def and PipeCalls.def")]
|
||||
out.append("""// One field id per GLContext accessor the backends actually read (plan B section 6.2:
|
||||
@@ -457,8 +759,8 @@ def gen_filled(accessors, calls):
|
||||
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
|
||||
// Fatal{UnmigratedPipeInput} (section 6.2.2).
|
||||
//
|
||||
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
|
||||
// itself lands in P1.
|
||||
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
|
||||
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
|
||||
""")
|
||||
out.append("enum class MGPipeInputField : Uint16 {")
|
||||
for name, _ in accessors:
|
||||
@@ -474,13 +776,17 @@ def gen_filled(accessors, calls):
|
||||
out.append(" \"%s\"," % name)
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each")
|
||||
out.append("// true has to be argued for in P1 when the fillers land: a sticky field is a field")
|
||||
out.append("// the poison cannot protect.")
|
||||
out.append("// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison")
|
||||
out.append("// cannot protect, so every true is argued for in Coverage.def's")
|
||||
out.append("// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).")
|
||||
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
|
||||
for name, _ in accessors:
|
||||
out.append(" false, // %s" % name)
|
||||
if name in sticky_map:
|
||||
out.append(" true, // %s: %s" % (name, sticky_map[name]))
|
||||
else:
|
||||
out.append(" false, // %s" % name)
|
||||
out.append("};")
|
||||
out.append("inline constexpr SizeT kMGPipeInputStickyFieldCount = %d;" % len(sticky))
|
||||
out.append("")
|
||||
out.append("// Which call is expected to have filled a field by the time a verb reads it. Names")
|
||||
out.append("// come from Coverage.def, so this table and the coverage table cannot disagree.")
|
||||
@@ -501,14 +807,166 @@ def gen_filled(accessors, calls):
|
||||
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);
|
||||
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
|
||||
: state.FilledGen[index] == state.CurrentVerbSerial;
|
||||
const Uint64 gen = state.FilledGen[index];
|
||||
if (gen == 0) return false;
|
||||
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
|
||||
}""")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
FUNCTION_POINTER_MEMBER_RE = re.compile(r"\(\s*\*\s*(\w+)\s*\)\s*\(")
|
||||
|
||||
|
||||
def parse_function_table():
|
||||
"""The function-pointer members of MG_Backend::GLFunctionsTable, in declaration order.
|
||||
Data members (PrefersCpuXfbPrimitiveAccounting) are not verbs and are skipped; comments
|
||||
are masked line by line."""
|
||||
text = read(FUNCTION_TABLE_HEADER)
|
||||
start = text.find("struct GLFunctionsTable {")
|
||||
if start < 0:
|
||||
sys.exit("%s: struct GLFunctionsTable is missing" % FUNCTION_TABLE_HEADER)
|
||||
members = []
|
||||
for line in text[start:].splitlines()[1:]:
|
||||
if re.match(r"^\s*};", line):
|
||||
break
|
||||
code = line.split("//", 1)[0]
|
||||
for name in FUNCTION_POINTER_MEMBER_RE.findall(code):
|
||||
members.append(name)
|
||||
if not members:
|
||||
sys.exit("%s: GLFunctionsTable has no function-pointer members" % FUNCTION_TABLE_HEADER)
|
||||
return members
|
||||
|
||||
|
||||
def parse_fill_points(accessors, table_members=None, text=None):
|
||||
"""FillPoints.def -> (verbs, classes, fields): verbs is [(verb, class)] in file order,
|
||||
classes is [class], fields is {class: [field]}. Refuses a verb set that is not exactly
|
||||
GLFunctionsTable's function-pointer members in declaration order, a verb in two
|
||||
classes, a class with no verbs, a field that is not an accessor, a duplicate
|
||||
(class, field) row, and a class row that names no class."""
|
||||
if text is None:
|
||||
text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
|
||||
if table_members is None:
|
||||
table_members = parse_function_table()
|
||||
|
||||
def block(macro):
|
||||
match = re.search(r"#define %s\(X\)(.*?)\n\n" % macro, text, re.S)
|
||||
if not match:
|
||||
sys.exit("FillPoints.def: %s is missing (or not followed by a blank line)" % macro)
|
||||
return match.group(1)
|
||||
|
||||
classes = re.findall(r"X\((\w+)\)", block("MGP_FILL_CLASS_LIST"))
|
||||
if len(classes) != len(set(classes)):
|
||||
sys.exit("FillPoints.def: a class is listed twice in MGP_FILL_CLASS_LIST")
|
||||
verbs = re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_VERB_LIST"))
|
||||
seen = set()
|
||||
for verb, cls in verbs:
|
||||
if verb in seen:
|
||||
sys.exit("FillPoints.def: verb %s is in two classes" % verb)
|
||||
seen.add(verb)
|
||||
if cls not in classes:
|
||||
sys.exit("FillPoints.def: verb %s names unknown class %s" % (verb, cls))
|
||||
verb_names = [verb for verb, _ in verbs]
|
||||
missing = [m for m in table_members if m not in seen]
|
||||
if missing:
|
||||
sys.exit("FillPoints.def: GLFunctionsTable member(s) without a verb row: %s" % ", ".join(missing))
|
||||
extra = [v for v in verb_names if v not in table_members]
|
||||
if extra:
|
||||
sys.exit("FillPoints.def: verb(s) that are not GLFunctionsTable members: %s" % ", ".join(extra))
|
||||
if verb_names != table_members:
|
||||
sys.exit("FillPoints.def: verb rows are not in GLFunctionsTable declaration order "
|
||||
"(first difference at %s)" % next(a for a, b in zip(verb_names, table_members) if a != b))
|
||||
for cls in classes:
|
||||
if not any(c == cls for _, c in verbs):
|
||||
sys.exit("FillPoints.def: class %s has no verbs" % cls)
|
||||
accessor_names = set(name for name, _ in accessors)
|
||||
fields = {cls: [] for cls in classes}
|
||||
for cls, field in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_FIELD_LIST")):
|
||||
if cls not in fields:
|
||||
sys.exit("FillPoints.def: field row names unknown class %s" % cls)
|
||||
if field not in accessor_names:
|
||||
sys.exit("FillPoints.def: %s is not an accessor in Coverage.def" % field)
|
||||
if field in fields[cls]:
|
||||
sys.exit("FillPoints.def: duplicate row (%s, %s)" % (cls, field))
|
||||
fields[cls].append(field)
|
||||
return verbs, classes, fields
|
||||
|
||||
|
||||
def gen_fill_points(accessors, sticky, verbs, classes, fields):
|
||||
field_index = {name: i for i, (name, _) in enumerate(accessors)}
|
||||
# Two words minimum (the P1 contract shape, headroom for the 64th field); grows on demand.
|
||||
words = max(2, (len(accessors) + 63) // 64)
|
||||
sticky_names = [name for name, _ in sticky]
|
||||
out = [banner("PipeFillPoints.inc", "G5b: the verb enum, the verb classes and their may-read field masks.",
|
||||
"FillPoints.def, Coverage.def and MG_Backend/BackendObject.h")]
|
||||
out.append("""// 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.
|
||||
""")
|
||||
out.append("enum class MGPipeVerb : Uint8 {")
|
||||
for verb, _ in verbs:
|
||||
out.append(" %s," % verb)
|
||||
out.append(" kVerbCount,")
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);")
|
||||
out.append("static_assert(kMGPipeVerbCount == %d, \"the GLFunctionsTable verb set moved\");" % len(verbs))
|
||||
out.append("")
|
||||
out.append("inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {")
|
||||
for verb, _ in verbs:
|
||||
out.append(" \"%s\"," % verb)
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("enum class MGPipeVerbClass : Uint8 {")
|
||||
for cls in classes:
|
||||
out.append(" %s," % cls)
|
||||
out.append(" kClassCount,")
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);")
|
||||
out.append("static_assert(kMGPipeVerbClassCount == %d, \"the verb class set moved\");" % len(classes))
|
||||
out.append("")
|
||||
out.append("inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {")
|
||||
for cls in classes:
|
||||
out.append(" \"%s\"," % cls)
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {")
|
||||
for verb, cls in verbs:
|
||||
out.append(" MGPipeVerbClass::%s, // %s" % (cls, verb))
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("// One bit per MGPipeInputField. The %d sticky fields are OR'ed into every class." % len(sticky))
|
||||
out.append("struct MGPipeFieldMask {")
|
||||
out.append(" Uint64 Words[%d];" % words)
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {")
|
||||
out.append(" const SizeT index = static_cast<SizeT>(field);")
|
||||
out.append(" return (mask.Words[index / 64] >> (index % 64)) & 1u;")
|
||||
out.append("}")
|
||||
out.append("")
|
||||
out.append("inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {")
|
||||
for cls in classes:
|
||||
bits = [0] * words
|
||||
names = fields[cls] + [n for n in sticky_names if n not in fields[cls]]
|
||||
for name in names:
|
||||
index = field_index[name]
|
||||
bits[index // 64] |= 1 << (index % 64)
|
||||
out.append(" // %s: %d fields (%d own + %d sticky)" % (cls, len(names), len(fields[cls]), len(names) - len(fields[cls])))
|
||||
out.append(" {{%s}}," % ", ".join("0x%016xull" % b for b in bits))
|
||||
out.append("};")
|
||||
out.append("")
|
||||
out.append("static_assert(kMGPipeInputFieldCount <= %d * 64, \"MGPipeFieldMask needs another word\");" % words)
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def gen_coverage(accessors, deltas, rows, calls):
|
||||
call_names = set(c.Name for c in calls)
|
||||
pseudo = {"kClientResolved", "kReverseChannel", "kStructuralHandle"}
|
||||
@@ -625,16 +1083,71 @@ def write(path, text, check, changed):
|
||||
handle.write(text)
|
||||
|
||||
|
||||
def expect_trip(name, fn):
|
||||
"""Runs one negative control; a gate that lets it through is the failure."""
|
||||
try:
|
||||
fn()
|
||||
except SystemExit as trip:
|
||||
print("gen_pipe: self-test %s: tripped as expected (%s)" % (name, str(trip).splitlines()[0][:100]))
|
||||
return 1
|
||||
print("gen_pipe: self-test %s: DID NOT TRIP" % name, file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def self_test(accessors):
|
||||
"""The negative controls (check_include_closure.py's shape): each gate must go red for
|
||||
its reason, and zero trips is itself an error."""
|
||||
canned_struct = "struct Canned {\n Uint32 A;\n Uint32 B, C;\n Uint8 Pad0[3];\n void F() { return; }\n};\n"
|
||||
controls = [
|
||||
("struct member without F(...)",
|
||||
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])),
|
||||
("F(...) that is not a member",
|
||||
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])),
|
||||
("payload with no struct",
|
||||
lambda: check_field_lists_cover_struct_members({"Nowhere": ["A"]}, ["Nowhere"], [canned_struct])),
|
||||
]
|
||||
fill_text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
|
||||
verb_row = re.compile(r"X\(\s*DrawArrays\s*,\s*kDraw\s*\)")
|
||||
field_row = re.compile(r"X\(\s*kDraw\s*,\s*GetBoundVertexArray\s*\)")
|
||||
if not verb_row.search(fill_text) or not field_row.search(fill_text):
|
||||
sys.exit("gen_pipe: self-test: FillPoints.def lost the rows the controls edit")
|
||||
controls.append(("verb missing from FillPoints.def", lambda: parse_fill_points(
|
||||
accessors, text=verb_row.sub("", fill_text, count=1))))
|
||||
controls.append(("verb that is not a GLFunctionsTable member", lambda: parse_fill_points(
|
||||
accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1))))
|
||||
controls.append(("field row naming a non-accessor", lambda: parse_fill_points(
|
||||
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
|
||||
trips = 0
|
||||
for name, fn in controls:
|
||||
trips += expect_trip(name, fn)
|
||||
# The positive control: the canned struct's exact list passes, and the parser sees the
|
||||
# padding member as padding and the function as not a member.
|
||||
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])
|
||||
if trips == 0:
|
||||
sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything")
|
||||
if trips != len(controls):
|
||||
sys.exit("gen_pipe: self-test: %d of %d negative controls did not trip" % (len(controls) - trips, len(controls)))
|
||||
print("gen_pipe: self-test: %d negative-control trip(s), positive control OK" % trips)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="do not write; exit 1 if regenerating would change anything")
|
||||
parser.add_argument("--self-test", action="store_true",
|
||||
help="run the negative controls (each gate must trip) and exit")
|
||||
args = parser.parse_args()
|
||||
|
||||
calls = parse_calls()
|
||||
payloads = parse_verify_payloads()
|
||||
check_call_payloads_have_field_lists(calls, payloads)
|
||||
accessors, deltas = parse_coverage()
|
||||
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
|
||||
accessors, deltas, sticky = parse_coverage()
|
||||
if args.self_test:
|
||||
return self_test(accessors)
|
||||
scan_live_accessors(accessors)
|
||||
verbs, classes, fields = parse_fill_points(accessors)
|
||||
rows = parse_inventory()
|
||||
|
||||
if not os.path.isdir(GENERATED_DIR):
|
||||
@@ -646,13 +1159,17 @@ def main():
|
||||
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeWire.inc"), gen_wire(calls), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
|
||||
gen_fill_points(accessors, sticky, verbs, classes, fields), args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
|
||||
write(os.path.join(GENERATED_DIR, "PipeSpanTable.inc"), gen_span_table(), args.check, changed)
|
||||
|
||||
screen = sum(1 for c in calls if c.IsScreen)
|
||||
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields"
|
||||
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors)))
|
||||
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields "
|
||||
"(%d sticky), %d verbs, %d classes"
|
||||
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
|
||||
len(verbs), len(classes)))
|
||||
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
|
||||
"%d structural handle, %d UNMAPPED"
|
||||
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
# MobileGL - scripts/symbol_report.py
|
||||
# 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
|
||||
"""Per-symbol nm/.text attribution between two libMobileGL.so builds.
|
||||
|
||||
P0.5's acceptance gate says every `nm --defined-only -S` delta has to be explainable per
|
||||
symbol (ROADMAP.md:16). The awkward part is that de-nesting a type renames every mangled
|
||||
name that mentions it - `ProgramObject::LinkArtifacts` -> `LinkArtifacts` shows up inside
|
||||
every `std::vector<...>` instantiation too - so a raw nm diff of a pure move looks
|
||||
catastrophic. `--strip-scope` folds those into a "renamed-only" bucket: same normalised
|
||||
demangled name, byte-identical size. What is left over is the report's real content.
|
||||
|
||||
python3 scripts/symbol_report.py --before old.so --after new.so
|
||||
python3 scripts/symbol_report.py --before old.so --after new.so \\
|
||||
--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::' \\
|
||||
--only-names 'TypeFacts,ResourceReflection,XfbVarying,LinkArtifacts,SpirvArtifacts' \\
|
||||
--markdown ~/w7/p05-symbol-report.md
|
||||
|
||||
GUARD RAILS - the comparison is meaningless unless both .so files were built with:
|
||||
* the same CMAKE_BUILD_TYPE (the visibility presets differ between configurations,
|
||||
CMakeLists.txt:578-598, so a Debug/Release pair "adds" thousands of symbols),
|
||||
* MOBILEGL_ENABLE_LTO OFF on both sides (CMakeLists.txt:108,137-146 - LTO merges and
|
||||
renames at will and nothing here is attributable afterwards),
|
||||
* the same compiler and standard library.
|
||||
The header of every report prints both paths and their byte sizes so a mismatched pair is
|
||||
visible in the output rather than only in the reader's assumptions.
|
||||
|
||||
This tool is informational by default (ARCHITECTURE.md:507, job name `monolith-symbol-report`
|
||||
ARCHITECTURE.md:568): with no gate flag it prints its report and exits 0, whatever it found.
|
||||
Three flags turn it into a hard gate, and P1's G1 uses the first two - the strangler's pull build
|
||||
has to stay byte-identical, and G1 spells that out as `added == removed == resized == renamed == 0`
|
||||
with a `.text` delta of zero:
|
||||
|
||||
python3 scripts/symbol_report.py --before base.so --after head.so \
|
||||
--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0
|
||||
|
||||
* `--fail-on-symbol-set-change` covers all FOUR buckets, not just added/removed: a resize at zero
|
||||
net delta and a rename at zero net delta are both source changes, and both are what a guard
|
||||
rewrite produces. It always compares at threshold 0, whatever `--threshold` is set to.
|
||||
* `--fail-on-added-bytes 0` means byte-identical, so a SHRUNK .text fails it too; a positive
|
||||
budget keeps the one-sided "must not grow by more than N" meaning. `--fail-on-text-delta` is
|
||||
the explicit spelling of the zero case for a run that wants no byte budget at all.
|
||||
|
||||
A gate that fires still writes its Markdown and JSON first: the report IS the diagnosis, and a
|
||||
CI job that failed before uploading its artifact is a job nobody can act on.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PREFIX = "symbol-report: "
|
||||
|
||||
# `nm --defined-only -S` prints "<addr> <size> <type> <name>", and "<addr> <type> <name>"
|
||||
# for a defined symbol whose size the object file does not carry (absolute symbols,
|
||||
# assembler labels).
|
||||
NM_SIZED_RE = re.compile(r"^([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(\S)\s+(.+)$")
|
||||
NM_UNSIZED_RE = re.compile(r"^([0-9a-fA-F]+)\s+(\S)\s+(.+)$")
|
||||
|
||||
# `size --format=sysv` prints "section size addr" rows plus a Total row.
|
||||
SIZE_ROW_RE = re.compile(r"^(\S+)\s+(\d+)(?:\s+(\d+))?\s*$")
|
||||
|
||||
INTERESTING_SECTIONS = (".text", ".data", ".bss", ".rodata", "Total")
|
||||
|
||||
|
||||
def say(message):
|
||||
print(PREFIX + message)
|
||||
|
||||
|
||||
def parse_nm(text):
|
||||
"""nm --defined-only -S transcript -> {mangled: (type, size_or_None)}."""
|
||||
symbols = {}
|
||||
for line in text.splitlines():
|
||||
line = line.rstrip()
|
||||
if not line:
|
||||
continue
|
||||
match = NM_SIZED_RE.match(line)
|
||||
if match:
|
||||
symbols[match.group(4)] = (match.group(3), int(match.group(2), 16))
|
||||
continue
|
||||
match = NM_UNSIZED_RE.match(line)
|
||||
if match:
|
||||
symbols[match.group(3)] = (match.group(2), None)
|
||||
return symbols
|
||||
|
||||
|
||||
def parse_size(text):
|
||||
"""size --format=sysv transcript -> {section: bytes}."""
|
||||
sections = {}
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("section"):
|
||||
continue
|
||||
match = SIZE_ROW_RE.match(stripped)
|
||||
if not match:
|
||||
continue
|
||||
sections[match.group(1)] = int(match.group(2))
|
||||
return sections
|
||||
|
||||
|
||||
def demangle(names, cxxfilt):
|
||||
"""Batch-demangle through c++filt; a plain identifier passes through unchanged."""
|
||||
ordered = list(names)
|
||||
if not ordered:
|
||||
return {}
|
||||
if not cxxfilt or not (shutil.which(cxxfilt) or os.path.isfile(cxxfilt)):
|
||||
return {name: name for name in ordered}
|
||||
completed = subprocess.run([cxxfilt], input="\n".join(ordered) + "\n",
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||
lines = completed.stdout.splitlines()
|
||||
if len(lines) != len(ordered):
|
||||
return {name: name for name in ordered}
|
||||
return dict(zip(ordered, lines))
|
||||
|
||||
|
||||
def parent_scope(prefix):
|
||||
"""`A::B::C::` -> `A::B::` (drop the last named component, keep the enclosing scope).
|
||||
|
||||
A de-nesting moves `A::B::C::X` to `A::B::X`, so folding the two spellings together
|
||||
means deleting the `C::` component, NOT the whole prefix - deleting the whole prefix
|
||||
would turn the before side into a bare `X` that no after-side name matches.
|
||||
"""
|
||||
body = prefix[:-2] if prefix.endswith("::") else prefix
|
||||
cut = body.rfind("::")
|
||||
return body[:cut + 2] if cut >= 0 else ""
|
||||
|
||||
|
||||
def normalise(name, strip_scopes, rename_map):
|
||||
"""Textual folding of the demangled name: this is what makes a de-nesting a rename."""
|
||||
for scope in strip_scopes:
|
||||
name = name.replace(scope, parent_scope(scope))
|
||||
for old, new in rename_map:
|
||||
name = name.replace(old, new)
|
||||
return name
|
||||
|
||||
|
||||
def build_side(path, nm_tool, size_tool, cxxfilt, strip_scopes, rename_map, canned=None):
|
||||
if canned is None:
|
||||
nm_text = subprocess.run([nm_tool, "--defined-only", "-S", path],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, check=True).stdout
|
||||
size_text = subprocess.run([size_tool, "--format=sysv", path],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, check=True).stdout
|
||||
else:
|
||||
nm_text, size_text = canned
|
||||
|
||||
raw = parse_nm(nm_text)
|
||||
demangled = demangle(raw.keys(), cxxfilt)
|
||||
table = {}
|
||||
for mangled, (kind, size) in raw.items():
|
||||
key = normalise(demangled.get(mangled, mangled), strip_scopes, rename_map)
|
||||
table.setdefault(key, []).append({"mangled": mangled, "type": kind,
|
||||
"size": size or 0,
|
||||
"demangled": demangled.get(mangled, mangled)})
|
||||
folded = {}
|
||||
for key, entries in table.items():
|
||||
folded[key] = {
|
||||
"size": sum(e["size"] for e in entries),
|
||||
"count": len(entries),
|
||||
"mangled": sorted(e["mangled"] for e in entries),
|
||||
"type": entries[0]["type"],
|
||||
}
|
||||
return folded, parse_size(size_text), len(raw)
|
||||
|
||||
|
||||
def bucket(before, after, only_names, threshold):
|
||||
def wanted(name):
|
||||
return not only_names or any(n in name for n in only_names)
|
||||
|
||||
removed, added, resized, renamed, unchanged = [], [], [], [], 0
|
||||
for name in sorted(set(before) | set(after)):
|
||||
b = before.get(name)
|
||||
a = after.get(name)
|
||||
if b is None:
|
||||
if wanted(name):
|
||||
added.append({"name": name, "delta": a["size"], "before": 0, "after": a["size"]})
|
||||
continue
|
||||
if a is None:
|
||||
if wanted(name):
|
||||
removed.append({"name": name, "delta": -b["size"], "before": b["size"], "after": 0})
|
||||
continue
|
||||
if a["size"] != b["size"] and abs(a["size"] - b["size"]) > threshold:
|
||||
if wanted(name):
|
||||
resized.append({"name": name, "delta": a["size"] - b["size"],
|
||||
"before": b["size"], "after": a["size"]})
|
||||
continue
|
||||
if a["mangled"] != b["mangled"]:
|
||||
# same normalised name, byte-identical size: the pure-move signature
|
||||
if wanted(name):
|
||||
renamed.append({"name": name, "delta": 0,
|
||||
"before": b["mangled"][0], "after": a["mangled"][0]})
|
||||
continue
|
||||
unchanged += 1
|
||||
for group in (removed, added, resized):
|
||||
group.sort(key=lambda row: (-abs(row["delta"]), row["name"]))
|
||||
renamed.sort(key=lambda row: row["name"])
|
||||
return removed, added, resized, renamed, unchanged
|
||||
|
||||
|
||||
def markdown_table(title, rows, columns):
|
||||
lines = ["", "### {} ({})".format(title, len(rows)), ""]
|
||||
if not rows:
|
||||
lines.append("_none_")
|
||||
lines.append("")
|
||||
return lines
|
||||
lines.append("| " + " | ".join(columns) + " |")
|
||||
lines.append("|" + "|".join(["---"] * len(columns)) + "|")
|
||||
for row in rows:
|
||||
lines.append("| " + " | ".join(str(cell).replace("|", "\\|") for cell in row) + " |")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
CANNED_BEFORE = ("""0000000000001000 0000000000000010 T MobileGL::MG_State::GLState::ProgramObject::LinkArtifacts::Reset()
|
||||
0000000000002000 0000000000000020 T MobileGL::MG_State::GLState::ProgramObject::Link()
|
||||
0000000000003000 0000000000000030 T MobileGL::Gone()
|
||||
0000000000004000 0000000000000040 T MobileGL::Grew()
|
||||
0000000000005000 T MobileGL::NoSize()
|
||||
""", """libBefore.so :
|
||||
section size addr
|
||||
.text 1000 100
|
||||
.data 200 2000
|
||||
.bss 300 3000
|
||||
Total 1500
|
||||
""")
|
||||
|
||||
CANNED_AFTER = ("""0000000000001000 0000000000000010 T MobileGL::MG_State::GLState::LinkArtifacts::Reset()
|
||||
0000000000002000 0000000000000020 T MobileGL::MG_State::GLState::ProgramObject::Link()
|
||||
0000000000004000 0000000000000050 T MobileGL::Grew()
|
||||
0000000000006000 0000000000000060 T MobileGL::BrandNew()
|
||||
0000000000005000 T MobileGL::NoSize()
|
||||
""", """libAfter.so :
|
||||
section size addr
|
||||
.text 1100 100
|
||||
.data 200 2000
|
||||
.bss 300 3000
|
||||
Total 1600
|
||||
""")
|
||||
|
||||
|
||||
def gate_failures(added, removed, resized, renamed, text_delta, fail_on_added_bytes,
|
||||
fail_on_symbol_set_change, fail_on_text_delta=False):
|
||||
"""The reasons this run should fail, in the order they are reported. Empty means green.
|
||||
|
||||
Pure, and takes the buckets rather than the file paths, so the self-test can drive it from the
|
||||
canned transcripts: a gate whose only test is a real build is a gate nobody re-tests.
|
||||
|
||||
P1's G1 is spelled `added == removed == resized == renamed == 0` and `.text` delta 0, so all
|
||||
FOUR buckets are part of --fail-on-symbol-set-change and a budget of 0 bytes means zero
|
||||
movement in EITHER direction. A shrunk .text and a resize with zero net delta are exactly the
|
||||
shapes a guard rewrite produces, and both used to walk straight through this function.
|
||||
"""
|
||||
reasons = []
|
||||
if fail_on_symbol_set_change and (added or removed or resized or renamed):
|
||||
reasons.append(
|
||||
"--fail-on-symbol-set-change: {} symbol(s) added, {} removed, {} resized, {} renamed. "
|
||||
"None of that is a property of the build machine, so any change here is a source "
|
||||
"change - name each one in the commit message or fix it.".format(
|
||||
len(added), len(removed), len(resized), len(renamed)))
|
||||
if fail_on_text_delta and text_delta != 0:
|
||||
reasons.append(
|
||||
"--fail-on-text-delta: .text moved by {:+d} bytes.".format(text_delta))
|
||||
if fail_on_added_bytes is not None:
|
||||
# A budget of zero is not "must not grow", it is "must not move": the pull build of a
|
||||
# strangler that got smaller is just as much a code change as one that got bigger, and G1
|
||||
# names the delta, not its sign. A positive budget keeps the older, one-sided meaning.
|
||||
if fail_on_added_bytes == 0 and text_delta != 0 and not fail_on_text_delta:
|
||||
reasons.append(
|
||||
"--fail-on-added-bytes 0: .text moved by {:+d} bytes (a budget of 0 means the "
|
||||
"section must be byte-identical, in either direction).".format(text_delta))
|
||||
elif fail_on_added_bytes > 0 and text_delta > fail_on_added_bytes:
|
||||
reasons.append(
|
||||
"--fail-on-added-bytes {}: .text grew by {} bytes.".format(
|
||||
fail_on_added_bytes, text_delta))
|
||||
return reasons
|
||||
|
||||
|
||||
def self_test():
|
||||
strip = ["MobileGL::MG_State::GLState::ProgramObject::"]
|
||||
before, before_sections, before_count = build_side(None, None, None, None, strip, [],
|
||||
canned=CANNED_BEFORE)
|
||||
after, after_sections, after_count = build_side(None, None, None, None, strip, [],
|
||||
canned=CANNED_AFTER)
|
||||
removed, added, resized, renamed, unchanged = bucket(before, after, [], 0)
|
||||
problems = []
|
||||
if before_count != 5 or after_count != 5:
|
||||
problems.append("nm parse counts: {} / {} (expected 5 / 5)".format(before_count, after_count))
|
||||
if [r["name"] for r in removed] != ["MobileGL::Gone()"]:
|
||||
problems.append("removed bucket: {}".format([r["name"] for r in removed]))
|
||||
if [r["name"] for r in added] != ["MobileGL::BrandNew()"]:
|
||||
problems.append("added bucket: {}".format([r["name"] for r in added]))
|
||||
if [(r["name"], r["delta"]) for r in resized] != [("MobileGL::Grew()", 0x10)]:
|
||||
problems.append("resized bucket: {}".format([(r["name"], r["delta"]) for r in resized]))
|
||||
# `ProgramObject::LinkArtifacts::Reset` -> `LinkArtifacts::Reset` folds to the same
|
||||
# normalised name at the same size: renamed-only, not added+removed.
|
||||
if [r["name"] for r in renamed] != ["MobileGL::MG_State::GLState::LinkArtifacts::Reset()"]:
|
||||
problems.append("renamed bucket: {}".format([r["name"] for r in renamed]))
|
||||
if unchanged != 2: # ProgramObject::Link() and NoSize()
|
||||
problems.append("unchanged count: {} (expected 2)".format(unchanged))
|
||||
if before_sections.get(".text") != 1000 or after_sections.get("Total") != 1600:
|
||||
problems.append("size --format=sysv parse: {} / {}".format(before_sections, after_sections))
|
||||
# The three gates, driven from the same canned transcripts: the after side adds one symbol,
|
||||
# removes one, resizes one, renames one and grows .text by 100, so each flag must fire, each
|
||||
# must stay quiet when it is not asked for, and --fail-on-added-bytes must accept a delta it
|
||||
# was told to tolerate.
|
||||
text_delta = after_sections.get(".text", 0) - before_sections.get(".text", 0)
|
||||
if gate_failures(added, removed, resized, renamed, text_delta, None, False):
|
||||
problems.append("gates fired with no flag set")
|
||||
if len(gate_failures(added, removed, resized, renamed, text_delta, None, True)) != 1:
|
||||
problems.append("--fail-on-symbol-set-change did not fire on 1 added + 1 removed")
|
||||
if len(gate_failures(added, removed, resized, renamed, text_delta, 0, False)) != 1:
|
||||
problems.append("--fail-on-added-bytes 0 did not fire on a +100 .text delta")
|
||||
if gate_failures(added, removed, resized, renamed, text_delta, 100, False):
|
||||
problems.append("--fail-on-added-bytes 100 fired on a +100 .text delta")
|
||||
if len(gate_failures([], [], [], [], text_delta, 0, True)) != 1:
|
||||
problems.append("an unchanged symbol set still tripped --fail-on-symbol-set-change")
|
||||
# The three shapes that used to walk through: a SHRUNK .text under a zero budget, a resize with
|
||||
# no net delta, and a rename with no net delta. G1 forbids all three by name.
|
||||
if not gate_failures([], [], [], [], -4096, 0, False):
|
||||
problems.append("--fail-on-added-bytes 0 passed a .text that SHRANK by 4096 bytes")
|
||||
if not gate_failures([], [], resized, [], 0, None, True):
|
||||
problems.append("--fail-on-symbol-set-change passed a resized symbol at zero net delta")
|
||||
if not gate_failures([], [], [], renamed, 0, None, True):
|
||||
problems.append("--fail-on-symbol-set-change passed a renamed symbol at zero net delta")
|
||||
if not gate_failures([], [], [], [], -4096, None, False, True):
|
||||
problems.append("--fail-on-text-delta passed a .text that SHRANK by 4096 bytes")
|
||||
if gate_failures([], [], [], [], 0, None, False, True):
|
||||
problems.append("--fail-on-text-delta fired on a zero .text delta")
|
||||
for problem in problems:
|
||||
say("self-test: " + problem)
|
||||
say("self-test: " + ("OK (2 canned transcripts, 5 buckets, 3 gates)" if not problems else "FAILED"))
|
||||
return 0 if not problems else 1
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--before", help="the baseline libMobileGL.so")
|
||||
parser.add_argument("--after", help="the libMobileGL.so under test")
|
||||
parser.add_argument("--nm", default="nm")
|
||||
parser.add_argument("--size", default="size")
|
||||
parser.add_argument("--cxxfilt", default="c++filt")
|
||||
parser.add_argument("--markdown", default=None, help="write the Markdown report here")
|
||||
parser.add_argument("--json", default=None, help="write the machine-readable result here")
|
||||
parser.add_argument("--strip-scope", action="append", default=[], metavar="PREFIX",
|
||||
help="de-nest this scope in demangled names before comparing: PREFIX "
|
||||
"'A::B::C::' rewrites every 'A::B::C::X' to 'A::B::X' (repeatable). "
|
||||
"This is what folds a de-nesting into a rename instead of an "
|
||||
"added+removed pair, including inside template arguments.")
|
||||
parser.add_argument("--rename-map", action="append", default=[], metavar="OLD=NEW",
|
||||
help="textual OLD=NEW substitution on demangled names (repeatable)")
|
||||
parser.add_argument("--only-names", default=None,
|
||||
help="comma-separated: list only symbols whose demangled text contains one")
|
||||
parser.add_argument("--threshold", type=int, default=0,
|
||||
help="ignore size deltas of at most this many bytes")
|
||||
parser.add_argument("--fail-on-added-bytes", type=int, default=None,
|
||||
help="exit non-zero when .text grew by more than this many bytes; 0 is "
|
||||
"special and means the section must be BYTE-IDENTICAL, so a shrink "
|
||||
"fails it too (that is what P1's G1 asks for)")
|
||||
parser.add_argument("--fail-on-text-delta", action="store_true",
|
||||
help="exit non-zero when .text moved at all, in either direction. The "
|
||||
"explicit spelling of what --fail-on-added-bytes 0 also does.")
|
||||
parser.add_argument("--fail-on-symbol-set-change", action="store_true",
|
||||
help="exit non-zero when any defined symbol was added, removed, resized or "
|
||||
"renamed (the four buckets of the report; --strip-scope decides which "
|
||||
"of added+removed vs renamed a de-nesting lands in, it does not "
|
||||
"excuse it). Compared at threshold 0 whatever --threshold says.")
|
||||
parser.add_argument("--self-test", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.self_test:
|
||||
return self_test()
|
||||
|
||||
if not args.before or not args.after:
|
||||
parser.error("--before and --after are required (or use --self-test)")
|
||||
|
||||
if args.fail_on_added_bytes is not None and args.fail_on_added_bytes < 0:
|
||||
parser.error("--fail-on-added-bytes takes a byte budget of 0 or more")
|
||||
|
||||
rename_map = []
|
||||
for entry in args.rename_map:
|
||||
if "=" not in entry:
|
||||
parser.error("--rename-map expects OLD=NEW, got " + entry)
|
||||
old, new = entry.split("=", 1)
|
||||
rename_map.append((old, new))
|
||||
only_names = [n.strip() for n in args.only_names.split(",")] if args.only_names else []
|
||||
|
||||
before, before_sections, before_count = build_side(
|
||||
args.before, args.nm, args.size, args.cxxfilt, args.strip_scope, rename_map)
|
||||
after, after_sections, after_count = build_side(
|
||||
args.after, args.nm, args.size, args.cxxfilt, args.strip_scope, rename_map)
|
||||
|
||||
removed, added, resized, renamed, unchanged = bucket(before, after, only_names, args.threshold)
|
||||
# --threshold is a REPORT control - "do not list resizes smaller than this" - and a gate that
|
||||
# read the thresholded buckets would quietly weaken itself the day someone raised it. The gates
|
||||
# always see the threshold-0 buckets.
|
||||
if args.threshold:
|
||||
gate_removed, gate_added, gate_resized, gate_renamed, _ = bucket(before, after, only_names, 0)
|
||||
else:
|
||||
gate_removed, gate_added, gate_resized, gate_renamed = removed, added, resized, renamed
|
||||
|
||||
before_text = before_sections.get(".text", 0)
|
||||
after_text = after_sections.get(".text", 0)
|
||||
delta = after_text - before_text
|
||||
percent = (100.0 * delta / before_text) if before_text else 0.0
|
||||
|
||||
say("before: {} ({} bytes on disk)".format(args.before, os.path.getsize(args.before)))
|
||||
say("after : {} ({} bytes on disk)".format(args.after, os.path.getsize(args.after)))
|
||||
if args.strip_scope:
|
||||
say("strip-scope: " + " ; ".join(args.strip_scope))
|
||||
if rename_map:
|
||||
say("rename-map: " + " ; ".join("{}={}".format(o, n) for o, n in rename_map))
|
||||
say(".text {} -> {} ({:+d}, {:+.3f}%)".format(before_text, after_text, delta, percent))
|
||||
for section in INTERESTING_SECTIONS:
|
||||
if section == ".text":
|
||||
continue
|
||||
b = before_sections.get(section)
|
||||
a = after_sections.get(section)
|
||||
if b is None and a is None:
|
||||
continue
|
||||
say("{} {} -> {} ({:+d})".format(section, b or 0, a or 0, (a or 0) - (b or 0)))
|
||||
say("{} -> {} defined symbols: {} added, {} removed, {} resized, {} renamed".format(
|
||||
before_count, after_count, len(added), len(removed), len(resized), len(renamed)))
|
||||
# The two counts differ by the folding: several mangled symbols can share one
|
||||
# normalised demangled name (local aliases, `.cold` parts, identical-COMDAT clones).
|
||||
say("{} -> {} normalised names, {} unchanged (name, size and mangling all identical)".format(
|
||||
len(before), len(after), unchanged))
|
||||
if only_names:
|
||||
say("listing restricted to names containing: " + ", ".join(only_names))
|
||||
gates = gate_failures(gate_added, gate_removed, gate_resized, gate_renamed, delta,
|
||||
args.fail_on_added_bytes, args.fail_on_symbol_set_change,
|
||||
args.fail_on_text_delta)
|
||||
if (args.fail_on_added_bytes is None and not args.fail_on_symbol_set_change
|
||||
and not args.fail_on_text_delta):
|
||||
say("no gate flag: informational run")
|
||||
elif args.threshold:
|
||||
say("gates compare at threshold 0 ({} added, {} removed, {} resized, {} renamed there); "
|
||||
"--threshold {} only shortens the report".format(
|
||||
len(gate_added), len(gate_removed), len(gate_resized), len(gate_renamed),
|
||||
args.threshold))
|
||||
|
||||
lines = ["# MobileGL symbol report", "",
|
||||
"| side | path | file bytes | .text |",
|
||||
"|---|---|---|---|",
|
||||
"| before | `{}` | {} | {} |".format(args.before, os.path.getsize(args.before), before_text),
|
||||
"| after | `{}` | {} | {} |".format(args.after, os.path.getsize(args.after), after_text),
|
||||
"",
|
||||
"`.text` {} -> {} ({:+d}, {:+.3f}%). {} -> {} defined symbols: "
|
||||
"{} added, {} removed, {} resized, {} renamed, {} unchanged.".format(
|
||||
before_text, after_text, delta, percent, before_count, after_count,
|
||||
len(added), len(removed), len(resized), len(renamed), unchanged)]
|
||||
lines += markdown_table("Removed", [(r["name"], r["before"]) for r in removed],
|
||||
["symbol", "bytes"])
|
||||
lines += markdown_table("Added", [(r["name"], r["after"]) for r in added],
|
||||
["symbol", "bytes"])
|
||||
lines += markdown_table("Resized", [(r["name"], r["before"], r["after"], "{:+d}".format(r["delta"]))
|
||||
for r in resized],
|
||||
["symbol", "before", "after", "delta"])
|
||||
lines += markdown_table("Renamed only (same size)",
|
||||
[(r["name"], r["before"], r["after"]) for r in renamed],
|
||||
["normalised symbol", "before mangling", "after mangling"])
|
||||
report = "\n".join(lines) + "\n"
|
||||
|
||||
if args.markdown:
|
||||
with open(args.markdown, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(report)
|
||||
say("markdown written to " + args.markdown)
|
||||
else:
|
||||
print(report)
|
||||
|
||||
if args.json:
|
||||
with open(args.json, "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump({"before": args.before, "after": args.after,
|
||||
"sections_before": before_sections, "sections_after": after_sections,
|
||||
"removed": removed, "added": added, "resized": resized,
|
||||
"renamed": renamed, "unchanged": unchanged}, handle, indent=2)
|
||||
handle.write("\n")
|
||||
say("json written to " + args.json)
|
||||
|
||||
# After the report is written, never before: a gate that fired is exactly when someone needs
|
||||
# the Markdown and the JSON.
|
||||
for reason in gates:
|
||||
say("FAIL " + reason)
|
||||
return 1 if gates else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -372,10 +372,12 @@ function(add_trace_replay_test CASE_NAME BACKEND)
|
||||
-P ${MOBILEGL_TRACE_ROOT}/run_trace_case.cmake)
|
||||
if(BACKEND STREQUAL "DirectGLES")
|
||||
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
|
||||
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330")
|
||||
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
||||
LABELS retrace)
|
||||
else()
|
||||
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
|
||||
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330")
|
||||
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
|
||||
LABELS retrace)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -127,3 +127,65 @@ endif()
|
||||
if(NOT replay_result EQUAL 0)
|
||||
message(FATAL_ERROR "${TRACE_CASE_NAME} ${TRACE_BACKEND} trace replay failed with status ${replay_result}")
|
||||
endif()
|
||||
|
||||
# --- MOBILEGL_PIPE_VERIFY: the third CI mode's own assertions (gates G3 and G8) --------------
|
||||
#
|
||||
# A retrace that exported MOBILEGL_PIPE_VERIFY=1 at a library which was never configured with
|
||||
# -DMOBILEGL_PIPE_VERIFY=ON is a no-op that looks exactly like a clean pass: the variable steers
|
||||
# nothing, the frames still match their goldens, and the case reports green having verified
|
||||
# nothing at all. The mode therefore has to prove it ran, and the only channel a `cmake -P` script
|
||||
# has for that is the library's own log.
|
||||
#
|
||||
# Three demands, all of them silent when MOBILEGL_PIPE_VERIFY is unset or "0", so an ordinary
|
||||
# retrace is untouched:
|
||||
# * mobilegl.log exists - the replay wrote one, so the library was loaded and logging;
|
||||
# * it carries "MGPipe: verify armed" - the comparator armed in THIS process;
|
||||
# * it carries none of the three MGPipe Fatals: Fatal{PipeVerifyDiffer (a push/pull divergence,
|
||||
# the thing the mode exists to find), Fatal{UnmigratedPipeInput (a backend read of a field the
|
||||
# verb's fill table does not list - fixed by adding the row to MG_Pipe/FillPoints.def, never by
|
||||
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT
|
||||
# or MOBILEGL_PIPE_POISON_OMIT). The third is in the regex on purpose even though D2 makes it
|
||||
# abort the process: with MOBILEGL_PIPE_VERIFY_FATAL=0 the abort is exactly what does not
|
||||
# happen, and a typo'd knob would otherwise leave the negative-control lane looking healthy.
|
||||
# The Fatal check is not redundant with the replay's exit status: MOBILEGL_PIPE_VERIFY_FATAL=0 is
|
||||
# the supported triage configuration, and there the divergence is logged and counted rather than
|
||||
# aborted, so the run would otherwise finish 0 with its own report in the log.
|
||||
if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "")
|
||||
set(pipe_verify_case "${TRACE_CASE_NAME} ${TRACE_BACKEND}")
|
||||
if("$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "0" OR "$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "false")
|
||||
message(STATUS "MGPipe verify: MOBILEGL_PIPE_VERIFY=$ENV{MOBILEGL_PIPE_VERIFY}, no verify assertions for ${pipe_verify_case}")
|
||||
elseif(NOT EXISTS "${mobilegl_log}")
|
||||
message(FATAL_ERROR
|
||||
"MOBILEGL_PIPE_VERIFY is set for ${pipe_verify_case} but the run wrote no ${mobilegl_log}, "
|
||||
"so there is no evidence the comparator ever armed. A verify retrace with no library log "
|
||||
"cannot be counted as a verify retrace.")
|
||||
else()
|
||||
file(READ "${mobilegl_log}" pipe_verify_log)
|
||||
string(FIND "${pipe_verify_log}" "MGPipe: verify armed" pipe_verify_armed_at)
|
||||
if(pipe_verify_armed_at EQUAL -1)
|
||||
message(FATAL_ERROR
|
||||
"MOBILEGL_PIPE_VERIFY is set for ${pipe_verify_case} and the library never reported "
|
||||
"\"MGPipe: verify armed\". Either this libMobileGL.so was not built with "
|
||||
"-DMOBILEGL_PIPE_VERIFY=ON - in which case the whole verify retrace lane is comparing "
|
||||
"nothing - or the runtime knob never reached the process. Check that the VERIFY "
|
||||
"runtime artifact is the one unpacked at ${MOBILEGL_LIBRARY}.")
|
||||
endif()
|
||||
file(STRINGS "${mobilegl_log}" pipe_verify_fatals
|
||||
REGEX "Fatal\\{(PipeVerifyDiffer|UnmigratedPipeInput|PipeVerifyBadKnob)")
|
||||
if(pipe_verify_fatals)
|
||||
foreach(line IN LISTS pipe_verify_fatals)
|
||||
message(STATUS "${line}")
|
||||
endforeach()
|
||||
list(LENGTH pipe_verify_fatals pipe_verify_fatal_count)
|
||||
message(FATAL_ERROR
|
||||
"${pipe_verify_case}: ${pipe_verify_fatal_count} MGPipe Fatal(s) under "
|
||||
"MOBILEGL_PIPE_VERIFY. Fatal{PipeVerifyDiffer, \"<Field>@<Verb>\"} is a real push/pull "
|
||||
"divergence and is recorded, not silenced; Fatal{UnmigratedPipeInput, \"<Field>@<Verb>\"} "
|
||||
"is a missing row in MG_Pipe/FillPoints.def's class table - add it, regenerate, rerun "
|
||||
"(never mark the field sticky); Fatal{PipeVerifyBadKnob, ...} is a misspelt "
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT - fix the spelling, the "
|
||||
"vocabularies are kMGPipeInputFieldNames[] and kMGPipeVerbNames[].")
|
||||
endif()
|
||||
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"cases": [
|
||||
{
|
||||
"name": "OpenRA",
|
||||
"verify": true,
|
||||
"trace_archive": "openra.tgz",
|
||||
"trace_file": "openra.trace",
|
||||
"golden": "openra.0000031249.png",
|
||||
@@ -27,6 +28,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-startup",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.21.4-startup.tgz",
|
||||
"golden": "minecraft-1.21.4-startup.0000092195.png",
|
||||
"target_call": 92195,
|
||||
@@ -34,6 +36,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-main-menu",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.21.4-main-menu.tgz",
|
||||
"golden": "minecraft-1.21.4-main-menu.0000481787.png",
|
||||
"alternate_golden": "minecraft-1.21.4-main-menu.0000481787-mali.png",
|
||||
@@ -42,6 +45,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.11-main-menu",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.21.11-main-menu.tgz",
|
||||
"golden": "minecraft-1.21.11-main-menu.0000205347.png",
|
||||
"target_call": 205347,
|
||||
@@ -49,6 +53,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.17-main-menu-854",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.17-main-menu-854.tgz",
|
||||
"golden": "minecraft-1.17-main-menu-854.0000117757.png",
|
||||
"target_call": 117757,
|
||||
@@ -56,6 +61,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-in-world",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.21.4-in-world.tgz",
|
||||
"golden": "minecraft-1.21.4-in-world.0000280000.png",
|
||||
"target_call": 280000
|
||||
@@ -70,6 +76,7 @@
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-fabric-sodium-in-world",
|
||||
"verify": true,
|
||||
"trace_archive": "minecraft-1.21.4-fabric-sodium-in-world.tgz",
|
||||
"golden": "minecraft-1.21.4-fabric-sodium-in-world.0000923340.png",
|
||||
"target_call": 923340,
|
||||
@@ -277,6 +284,7 @@
|
||||
},
|
||||
{
|
||||
"name": "improved-transparency-minecraft-26.3",
|
||||
"verify": true,
|
||||
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
|
||||
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
|
||||
"target_call": 2667619,
|
||||
|
||||
@@ -26,6 +26,16 @@ def load_trace_case_manifest(path=TRACE_CASES_JSON):
|
||||
for key in ("trace_archive", "trace_file", "golden", "target_call", "width", "height"):
|
||||
if key not in merged:
|
||||
raise ValueError(f"{key} is required for {name}")
|
||||
# "verify" opts a case into the MOBILEGL_PIPE_VERIFY retrace subset. It is checked here
|
||||
# rather than where the matrix is built so that a typo is a loud manifest error in every
|
||||
# consumer (the cmake emitter included) instead of a subset that is quietly one case short.
|
||||
verify = merged.get("verify", False)
|
||||
if not isinstance(verify, bool):
|
||||
raise ValueError(f"verify must be true or false for {name}")
|
||||
if verify and not merged.get("ci", True):
|
||||
raise ValueError(
|
||||
f"{name} is marked verify but excluded from CI, so the verify matrix would drop it"
|
||||
)
|
||||
cases.append(merged)
|
||||
return {"defaults": defaults, "cases": cases}
|
||||
|
||||
@@ -72,6 +82,16 @@ def ci_trace_cases(cases):
|
||||
return [case for case in cases if case.get("ci", True)]
|
||||
|
||||
|
||||
def verify_trace_cases(cases):
|
||||
"""The subset the third CI mode retraces.
|
||||
|
||||
The verify build compares two state models at every verb boundary and again at every accessor
|
||||
read, which the design budgets at 5-10x, so the per-push lane runs a named subset and the full
|
||||
79-case sweep happens at the phase exit and on workflow_dispatch.
|
||||
"""
|
||||
return [case for case in cases if case.get("verify", False)]
|
||||
|
||||
|
||||
def ci_backends(case):
|
||||
backends = case.get("ci_backends")
|
||||
if backends is None:
|
||||
@@ -98,6 +118,10 @@ def github_test_matrix(cases):
|
||||
}
|
||||
|
||||
|
||||
def github_verify_matrix(cases):
|
||||
return github_test_matrix(verify_trace_cases(cases))
|
||||
|
||||
|
||||
def github_apk_matrix(cases):
|
||||
backends = {
|
||||
"DirectGLES": {"name": "DirectGLES", "gpu": "software"},
|
||||
@@ -158,6 +182,7 @@ def parse_args():
|
||||
choices=(
|
||||
"names",
|
||||
"github-test-matrix",
|
||||
"github-verify-matrix",
|
||||
"github-apk",
|
||||
"github-apk-matrix",
|
||||
"fixture-files",
|
||||
@@ -177,6 +202,8 @@ def main():
|
||||
print(json.dumps([case["name"] for case in cases], separators=(",", ":")))
|
||||
elif args.format == "github-test-matrix":
|
||||
print(json.dumps(github_test_matrix(cases), separators=(",", ":")))
|
||||
elif args.format == "github-verify-matrix":
|
||||
print(json.dumps(github_verify_matrix(cases), separators=(",", ":")))
|
||||
elif args.format == "github-apk":
|
||||
print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":")))
|
||||
elif args.format == "github-apk-matrix":
|
||||
|
||||
Reference in New Issue
Block a user