mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4322427e78 | ||
|
|
203d4bce5e | ||
|
|
761114d022 | ||
|
|
9caf34d5b1 | ||
|
|
5e676b338b | ||
|
|
db01bfa3e8 | ||
|
|
cc3dcfd80e | ||
|
|
d9556ff041 | ||
|
|
39f21e52ea | ||
|
|
0e933b8f2f |
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=trace-fixture-lib.sh
|
||||
. "${script_dir}/trace-fixture-lib.sh"
|
||||
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||
echo "usage: $0 <trace-case> [fixture-dir]" >&2
|
||||
exit 2
|
||||
@@ -66,6 +62,57 @@ if [ "${case_name}" = "OpenRA" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_file_from_mirror() {
|
||||
local file="$1"
|
||||
local url="$2"
|
||||
@@ -154,11 +201,6 @@ fetch_file_from_mirror() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Files no mirror could serve, even after retrying every mirror. Only these fall
|
||||
# back to Git LFS, so a mirror that served the rest of the case still spares
|
||||
# GitHub the bandwidth for those files.
|
||||
mirror_failures=()
|
||||
|
||||
fetch_from_mirror() {
|
||||
mkdir -p "${fixture_dir}"
|
||||
for file in "${files[@]}"; do
|
||||
@@ -177,19 +219,17 @@ fetch_from_mirror() {
|
||||
echo "Mirror did not serve ${name}; trying the next mirror" >&2
|
||||
done
|
||||
if [ "${fetched}" -ne 1 ]; then
|
||||
mirror_failures+=("${file}")
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
[ "${#mirror_failures[@]}" -eq 0 ]
|
||||
}
|
||||
|
||||
if fetch_from_mirror; then
|
||||
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
||||
else
|
||||
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
|
||||
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
|
||||
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
|
||||
git lfs install --local
|
||||
git lfs pull --include="${fallback_include}" --exclude=""
|
||||
git lfs pull --include="${include}" --exclude=""
|
||||
fi
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cache-side helper for trace fixtures.
|
||||
#
|
||||
# key <case> [fixture-dir] derive the actions/cache key and path list
|
||||
# verify <case> [fixture-dir] check restored fixtures against their pointers
|
||||
# reset <case> [fixture-dir] drop restored fixtures, leaving the pointers
|
||||
#
|
||||
# The cache key is content-addressed on the Git LFS pointer oids tracked at
|
||||
# HEAD, which are readable from a plain checkout without smudging. Fixture
|
||||
# content therefore maps 1:1 onto a key: unchanged content hits, changed
|
||||
# content is a new key and thus a miss, and the download path handles it. The
|
||||
# key deliberately carries no restore-keys prefix in the workflow - a fixture
|
||||
# that does not match the pointer exactly must never be restored.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=trace-fixture-lib.sh
|
||||
. "${script_dir}/trace-fixture-lib.sh"
|
||||
|
||||
# Bump when the key derivation changes in a way that must invalidate old
|
||||
# entries; the content digest alone would not notice a format change.
|
||||
key_schema="v1"
|
||||
|
||||
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
|
||||
echo "usage: $0 <key|verify|reset> <trace-case> [fixture-dir]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command_name="$1"
|
||||
case_name="$2"
|
||||
fixture_dir="${3:-tools/trace_replay/fixtures}"
|
||||
python_bin="${PYTHON:-python3}"
|
||||
|
||||
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
|
||||
python_bin=python
|
||||
fi
|
||||
|
||||
mapfile -t files < <(trace_fixture_files "${case_name}" "${fixture_dir}" "${python_bin}")
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "no fixture files declared for trace case: ${case_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Writes "name=value" to $GITHUB_OUTPUT when running under Actions, and to
|
||||
# stdout otherwise so the script stays runnable (and testable) off-CI.
|
||||
emit_output() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
if [[ "${value}" == *$'\n'* ]]; then
|
||||
local delimiter="ghadelim_$(date +%s%N)_$$"
|
||||
{
|
||||
printf '%s<<%s\n' "${name}" "${delimiter}"
|
||||
printf '%s\n' "${value}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
printf '%s=%s\n' "${name}" "${value}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
fi
|
||||
printf '%s=%s\n' "${name}" "${value}"
|
||||
}
|
||||
|
||||
sanitize_case() {
|
||||
printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g'
|
||||
}
|
||||
|
||||
case "${command_name}" in
|
||||
key)
|
||||
manifest=""
|
||||
for file in "${files[@]}"; do
|
||||
# A case whose fixtures are committed directly rather than through Git LFS
|
||||
# (OpenRA) has no pointer oid to key on, and nothing to download either.
|
||||
# Report it as uncacheable so the workflow skips the cache entirely.
|
||||
if ! metadata="$(get_lfs_metadata "${file}" 2>/dev/null)"; then
|
||||
echo "trace case ${case_name} is not stored in Git LFS; skipping fixture cache" >&2
|
||||
emit_output "cacheable" "false"
|
||||
emit_output "key" ""
|
||||
exit 0
|
||||
fi
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
manifest+="$(basename "${file}") ${expected_oid} ${expected_size}"$'\n'
|
||||
done
|
||||
|
||||
digest="$(printf '%s' "${manifest}" | sha256sum | awk '{ print substr($1, 1, 16) }')"
|
||||
safe_case="$(sanitize_case "${case_name}")"
|
||||
|
||||
emit_output "cacheable" "true"
|
||||
emit_output "key" "trace-fixture-${key_schema}-${safe_case}-${digest}"
|
||||
emit_output "paths" "$(printf '%s\n' "${files[@]}")"
|
||||
;;
|
||||
|
||||
verify)
|
||||
for file in "${files[@]}"; do
|
||||
metadata="$(get_lfs_metadata "${file}")"
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
|
||||
done
|
||||
echo "Verified ${#files[@]} fixture file(s) for ${case_name} against the tracked Git LFS pointers."
|
||||
;;
|
||||
|
||||
reset)
|
||||
# Put the working tree back to the pointer files a fresh checkout would
|
||||
# have, so that a rejected cache entry falls through to exactly the same
|
||||
# download path a cache miss takes.
|
||||
for file in "${files[@]}"; do
|
||||
rm -f "${file}" "${file}.tmp"
|
||||
done
|
||||
git checkout -- "${files[@]}"
|
||||
echo "Reset ${#files[@]} fixture file(s) for ${case_name} to their tracked Git LFS pointers."
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "unknown command: ${command_name}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for trace-fixture handling: reading the in-tree Git LFS pointer
|
||||
# metadata and verifying a fixture file against it. Sourced by
|
||||
# fetch-trace-fixture-lfs.sh (verify after download) and by
|
||||
# trace-fixture-cache.sh (cache key derivation and verify after cache restore),
|
||||
# so both paths agree on what a valid fixture is.
|
||||
|
||||
# Reads the Git LFS pointer tracked at HEAD for a fixture path and prints
|
||||
# "<oid> <size>". Fails if the tracked blob is not a well-formed LFS pointer.
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
# Checks an on-disk fixture against the size and SHA-256 from its LFS pointer.
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Prints the fixture file paths of a trace case, one per line. Strips CR so the
|
||||
# result is usable when python emits CRLF (Git Bash on Windows).
|
||||
trace_fixture_files() {
|
||||
local case_name="$1"
|
||||
local fixture_dir="$2"
|
||||
local python_bin="${3:-python3}"
|
||||
|
||||
"${python_bin}" tools/trace_replay/trace_cases.py \
|
||||
--format fixture-files \
|
||||
--case "${case_name}" \
|
||||
--fixture-root "${fixture_dir}" | tr -d '\r'
|
||||
}
|
||||
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
|
||||
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
|
||||
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
||||
|
||||
+43
-273
@@ -6,30 +6,11 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
- codex/fix-github-ci
|
||||
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
|
||||
# workflow keeps running for docs: it hosts the doc citation lint.
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
|
||||
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
CCACHE_COMPRESS: "true"
|
||||
@@ -37,7 +18,6 @@ jobs:
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -55,20 +35,18 @@ jobs:
|
||||
distribution: zulu
|
||||
java-version: '17'
|
||||
|
||||
- name: Android replay lifecycle regression
|
||||
run: python3 tools/trace_replay/test_android_lifecycle.py
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-version: 8.10.2
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
@@ -81,8 +59,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -97,13 +73,7 @@ jobs:
|
||||
run: python update_glslang_sources.py
|
||||
|
||||
- name: Build plugin APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -145,14 +115,7 @@ jobs:
|
||||
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
|
||||
|
||||
- name: Build retrace APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
|
||||
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -162,28 +125,6 @@ jobs:
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
# Rewrite one rolling entry per job on the default branch. The upload stays
|
||||
# cumulative - it carries every object restored at the top of this run plus
|
||||
# the few TUs that actually changed - but Actions cache keys are immutable,
|
||||
# so the superseded blob has to be released before the same key can be
|
||||
# re-uploaded. Running after the build means a failed build leaves the
|
||||
# existing entry untouched. The other trigger branches restore this entry
|
||||
# rather than each writing a ~4 GB one of their own.
|
||||
- name: Release superseded ccache entry
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CACHE_KEY: ${{ runner.os }}-apk-${{ 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 }}-apk-${{ github.job }}-ccache-v1
|
||||
|
||||
- name: Verify APK metadata and packaging
|
||||
run: |
|
||||
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
|
||||
@@ -234,8 +175,6 @@ jobs:
|
||||
name: trace case matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
outputs:
|
||||
android: ${{ steps.trace-cases.outputs.android }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
@@ -246,12 +185,7 @@ jobs:
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: |
|
||||
python3 tools/trace_replay/trace_cases.py --self-test-transport-matrices
|
||||
matrix_format=github-apk-matrix
|
||||
if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then
|
||||
matrix_format=github-apk-transport-matrix
|
||||
fi
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format "${matrix_format}")" >> "$GITHUB_OUTPUT"
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
@@ -267,41 +201,9 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Derive trace fixture cache key
|
||||
id: fixture-key
|
||||
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
|
||||
|
||||
- name: Restore trace fixture cache
|
||||
id: fixture-cache
|
||||
if: steps.fixture-key.outputs.cacheable == 'true'
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.fixture-key.outputs.paths }}
|
||||
key: ${{ steps.fixture-key.outputs.key }}
|
||||
|
||||
- name: Verify restored trace fixture
|
||||
id: fixture-verify
|
||||
if: steps.fixture-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
|
||||
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
|
||||
fi
|
||||
|
||||
- name: Fetch trace fixture
|
||||
if: steps.fixture-verify.outputs.ok != 'true'
|
||||
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
|
||||
|
||||
- name: Save trace fixture cache
|
||||
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ steps.fixture-key.outputs.paths }}
|
||||
key: ${{ steps.fixture-key.outputs.key }}
|
||||
|
||||
- name: Stage trace fixture
|
||||
run: |
|
||||
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
@@ -335,8 +237,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -364,7 +264,7 @@ jobs:
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
retrace:
|
||||
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
|
||||
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
@@ -374,8 +274,6 @@ jobs:
|
||||
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||
timeout-minutes: 75
|
||||
env:
|
||||
MOBILEGL_CI_TRANSPORT: ${{ matrix.transport || 'monolith' }}
|
||||
MOBILEGL_CI_LANE: ${{ matrix.lane || 'monolith-control' }}
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
@@ -383,9 +281,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
matrix:
|
||||
backend:
|
||||
- name: DirectGLES
|
||||
gpu: software
|
||||
- name: DirectVulkan
|
||||
gpu: lavapipe
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
steps:
|
||||
- name: Set swap space
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 8
|
||||
@@ -414,8 +318,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -454,7 +356,7 @@ jobs:
|
||||
--profile pixel_6 \
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
- name: Launch emulator
|
||||
- name: Launch Emulator
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh start \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
@@ -465,49 +367,18 @@ jobs:
|
||||
|
||||
- name: Retrace and validate
|
||||
env:
|
||||
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
run: |
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
extra_retrace_args=()
|
||||
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--require-inproc
|
||||
)
|
||||
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
|
||||
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
|
||||
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
|
||||
# environment by the launcher and read by the client, which is where they belong.
|
||||
#
|
||||
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
|
||||
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
|
||||
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
|
||||
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
|
||||
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
|
||||
# into the CLIENT's process and SetWindowHandle is refused on the way across with
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--use-pbuffer
|
||||
--require-spawn
|
||||
)
|
||||
else
|
||||
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
|
||||
fi
|
||||
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
||||
# min-filter downgrading on ANGLE llvmpipe.
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
|
||||
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
|
||||
fi
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
|
||||
fi
|
||||
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--coherent-as-flush)
|
||||
fi
|
||||
@@ -540,24 +411,6 @@ jobs:
|
||||
run_retrace || retrace_status=$?
|
||||
if [ "${retrace_status}" -eq 75 ]; then
|
||||
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
||||
# Surface-lost is retried rather than failed, so it would otherwise
|
||||
# be invisible. Report it per job - a healthy run prints nothing and
|
||||
# a rate spike shows up as a row per affected case.
|
||||
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
|
||||
surface_lost_retries=0
|
||||
if [ -f "${reason_file}" ]; then
|
||||
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
|
||||
fi
|
||||
if [ "${surface_lost_retries}" -gt 0 ]; then
|
||||
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
|
||||
>> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
# The restart truncates EMULATOR_LOG, and the attempt that lost the
|
||||
# emulator is the one worth reading - the retry usually only shows
|
||||
# the wreckage. Keep the first attempt's log before it is clobbered.
|
||||
if [ -f "${EMULATOR_LOG}" ]; then
|
||||
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
|
||||
fi
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
@@ -581,12 +434,6 @@ jobs:
|
||||
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
|
||||
mkdir -p "${result_dir}"
|
||||
python3 - "${result_dir}/lane.json" <<'PY'
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
Path(sys.argv[1]).write_text(json.dumps({"lane": os.environ["MOBILEGL_CI_LANE"],
|
||||
"transport": os.environ["MOBILEGL_CI_TRANSPORT"]}, indent=2))
|
||||
PY
|
||||
if [ -s "${{ matrix.case.golden }}" ]; then
|
||||
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
|
||||
fi
|
||||
@@ -603,15 +450,8 @@ jobs:
|
||||
if [ -f "${EMULATOR_LOG}" ]; then
|
||||
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
|
||||
fi
|
||||
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
|
||||
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
|
||||
fi
|
||||
# A vanished emulator looks identical whether the host OOM killer took
|
||||
# qemu or the renderer faulted. These two say which.
|
||||
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
|
||||
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
|
||||
|
||||
- name: Stop emulator
|
||||
- name: Stop Emulator
|
||||
if: always()
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
@@ -623,7 +463,7 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
path: android-retrace-result/**
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -632,8 +472,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: retrace
|
||||
if: always()
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -648,73 +486,25 @@ jobs:
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Download Android monolith control results
|
||||
- name: Download Android retrace results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
|
||||
path: retrace-artifacts/monolith-control
|
||||
pattern: MobileGL-android-retrace-result-*
|
||||
path: retrace-artifacts
|
||||
|
||||
- name: Download Android inproc acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-*
|
||||
path: retrace-artifacts/inproc-acceptance
|
||||
|
||||
- name: Download Android spawn acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
|
||||
path: retrace-artifacts/spawn-acceptance
|
||||
|
||||
- name: Render monolith control summary
|
||||
- name: Render retrace summary
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/monolith-control \
|
||||
--output-dir android-retrace-summary/monolith-control \
|
||||
--title "MobileGL Android monolith control" \
|
||||
--group-label "Android Emulator / monolith control" \
|
||||
--html mobilegl-android-monolith-control.html
|
||||
--input retrace-artifacts \
|
||||
--output-dir android-retrace-summary \
|
||||
--title "MobileGL Android retrace overview" \
|
||||
--group-label "Android Emulator" \
|
||||
--html mobilegl-android-retrace-overview.html
|
||||
|
||||
- name: Render inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/inproc-acceptance \
|
||||
--output-dir android-retrace-summary/inproc-acceptance \
|
||||
--title "MobileGL Android inproc acceptance" \
|
||||
--group-label "Android Emulator / inproc acceptance" \
|
||||
--html mobilegl-android-inproc-acceptance.html
|
||||
|
||||
- name: Render spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
|
||||
|
||||
- name: Upload Android monolith control summary
|
||||
- name: Upload Android retrace summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-monolith-control-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
|
||||
path: android-retrace-summary/mobilegl-android-retrace-overview.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -730,61 +520,41 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fail before deletion if either paginated inventory cannot be read.
|
||||
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' \
|
||||
> "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv' \
|
||||
> "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
declare -A failed_cases=()
|
||||
while IFS= read -r job_name; do
|
||||
case_name="${job_name#retrace (*, }"
|
||||
case_name="${case_name%)}"
|
||||
failed_cases["${case_name}"]=1
|
||||
done < "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
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'
|
||||
)
|
||||
|
||||
if ((${#failed_cases[@]})); then
|
||||
echo "Retaining fixtures and results for failed retrace case(s):"
|
||||
echo "Retaining fixtures for failed retrace case(s):"
|
||||
printf ' %s\n' "${!failed_cases[@]}"
|
||||
else
|
||||
echo "All retrace jobs succeeded; nothing needs to be retained."
|
||||
echo "All retrace jobs succeeded; no fixtures need to be retained."
|
||||
fi
|
||||
|
||||
deleted=0
|
||||
retained=0
|
||||
while IFS=$'\t' read -r artifact_id artifact_name; do
|
||||
keep=0
|
||||
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
|
||||
case_name="${artifact_name#MobileGL-trace-fixture-}"
|
||||
if [[ -v "failed_cases[${case_name}]" ]]; then
|
||||
keep=1
|
||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||
((retained += 1))
|
||||
continue
|
||||
fi
|
||||
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
|
||||
# The result artifact carries mobilegl.log, retrace.log, logcat,
|
||||
# the emulator log and the actual/diff images - the only record of
|
||||
# why a retrace failed. Its name ends in -<backend>-<case>, so a
|
||||
# suffix match on the case name keeps both backends' results for a
|
||||
# case that failed on either of them, which is what a comparison
|
||||
# needs. The match is anchored at the end, so a case name that is a
|
||||
# prefix of a longer one does not retain the longer one's results.
|
||||
for case_name in "${!failed_cases[@]}"; do
|
||||
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
|
||||
keep=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ((keep)); then
|
||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||
((retained += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Deleting ${artifact_name} (${artifact_id})"
|
||||
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
||||
((deleted += 1))
|
||||
done < "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
|
||||
)
|
||||
|
||||
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
|
||||
|
||||
+45
-3186
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,3 @@ MobileGL/MG*/cmake-build*
|
||||
/android-plugin/app/src/trace/jniLibs
|
||||
/android-plugin/local.properties
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/.gradle
|
||||
|
||||
+3
-9
@@ -7,6 +7,9 @@
|
||||
[submodule "3rdparty/SPIRV-Cross"]
|
||||
path = 3rdparty/SPIRV-Cross
|
||||
url = https://github.com/KhronosGroup/SPIRV-Cross.git
|
||||
[submodule "include/FastSTL"]
|
||||
path = include/FastSTL
|
||||
url = https://github.com/MobileGL-Dev/FastSTL.git
|
||||
[submodule "3rdparty/tracy"]
|
||||
path = 3rdparty/tracy
|
||||
url = https://github.com/wolfpld/tracy.git
|
||||
@@ -28,12 +31,3 @@
|
||||
[submodule "3rdparty/apitrace"]
|
||||
path = 3rdparty/apitrace
|
||||
url = https://github.com/MobileGL-Dev/apitrace.git
|
||||
[submodule "3rdparty/asio"]
|
||||
path = 3rdparty/asio
|
||||
url = https://github.com/chriskohlhoff/asio.git
|
||||
[submodule "include/ska"]
|
||||
path = include/ska
|
||||
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||
[submodule "3rdparty/flatbuffers"]
|
||||
path = 3rdparty/flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: c8036190fc...10935bb5e4
Vendored
-1
Submodule 3rdparty/asio deleted from 8806a6803c
Vendored
-1
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
Vendored
+1
-1
Submodule 3rdparty/glslang updated: d89cf443bc...26fe5ceb45
+4
-564
@@ -4,129 +4,20 @@ project("MobileGL")
|
||||
|
||||
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
|
||||
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
|
||||
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
|
||||
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
|
||||
# skips cleanly where there is none. Registered under the `integration-gpu`
|
||||
# ctest label so a run can select or exclude them.
|
||||
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
|
||||
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
|
||||
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
|
||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
|
||||
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
|
||||
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
|
||||
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
|
||||
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
|
||||
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
|
||||
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
|
||||
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
|
||||
# only make sense when the two roles share an address space. It is a SUPERSET, never a
|
||||
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
|
||||
# only decides whether the shims are compiled in.
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" 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)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||
# the whole migration window; it retires with the pull path itself at P13.
|
||||
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||
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")
|
||||
|
||||
if (ANDROID)
|
||||
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
|
||||
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
||||
|
||||
# ------- Android API level policy: minimum 26, decided here and only here -------
|
||||
# MobileGL ships against API 26: the codebase must not use any API introduced
|
||||
# after 26. That usage constraint is enforced where it is real - the shipping
|
||||
# gradle build compiles at minSdk 26, where a newer API is simply undeclared
|
||||
# and fails to compile. Configuring at a HIGHER level is therefore allowed
|
||||
# (nothing in the tree may rely on it), but a LOWER level would change the
|
||||
# libc contract underneath the shipped library and is refused.
|
||||
#
|
||||
# This has to live at configure time because the level cannot be corrected
|
||||
# from a source header. A `#define __ANDROID_API__ 26` in a common header
|
||||
# only rewrites the macro for the bionic headers that happen to be included
|
||||
# after it; any libc++ header pulled in earlier has already latched its
|
||||
# feature macros at the real configure-time level. libc++ and bionic then
|
||||
# disagree about which symbols exist - libc++ calls e.g.
|
||||
# pthread_cond_clockwait while bionic, re-read at the lowered level, has
|
||||
# hidden its declaration. MobileGL/Defines.h carried exactly that pin from
|
||||
# the first commit until it was removed; this guard is what replaces it.
|
||||
#
|
||||
# Read the level back from the compiler target triple first. Its trailing
|
||||
# number (aarch64-none-linux-android26) is precisely what clang turns into
|
||||
# __ANDROID_API__, so it cannot disagree with the compile itself, and it is
|
||||
# already past every NDK normalisation step - codename aliases, "latest",
|
||||
# and per-ABI minimum pull-ups. ANDROID_PLATFORM_LEVEL is the fallback for
|
||||
# generators/languages where the triple variable is not populated.
|
||||
#
|
||||
# Note CMAKE_SYSTEM_VERSION is deliberately NOT consulted: it holds the API
|
||||
# level only under the NDK's newer toolchain path, and is a meaningless 1
|
||||
# when ANDROID_USE_LEGACY_TOOLCHAIN_FILE is on (which is what AGP has been
|
||||
# defaulting to). Reading it would fail every legacy-mode build.
|
||||
set(MOBILEGL_ANDROID_API_LEVEL 26)
|
||||
|
||||
set(_mobilegl_android_api "")
|
||||
foreach (_mobilegl_api_triple "${CMAKE_CXX_COMPILER_TARGET}"
|
||||
"${CMAKE_C_COMPILER_TARGET}")
|
||||
if (NOT _mobilegl_android_api AND
|
||||
_mobilegl_api_triple MATCHES "-android([0-9]+)$")
|
||||
set(_mobilegl_android_api "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach (_mobilegl_api_var ANDROID_PLATFORM_LEVEL ANDROID_NATIVE_API_LEVEL
|
||||
ANDROID_PLATFORM)
|
||||
if (NOT _mobilegl_android_api AND ${_mobilegl_api_var})
|
||||
string(REGEX REPLACE "^android-" ""
|
||||
_mobilegl_android_api "${${_mobilegl_api_var}}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (NOT _mobilegl_android_api MATCHES "^[0-9]+$")
|
||||
message(FATAL_ERROR
|
||||
"MobileGL: could not determine the Android API level (got "
|
||||
"\"${_mobilegl_android_api}\"). Configure with the NDK toolchain "
|
||||
"file and -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL}.")
|
||||
elseif (_mobilegl_android_api LESS MOBILEGL_ANDROID_API_LEVEL)
|
||||
message(FATAL_ERROR
|
||||
"MobileGL requires at least Android API ${MOBILEGL_ANDROID_API_LEVEL}, "
|
||||
"but this build resolved to API ${_mobilegl_android_api}.\n"
|
||||
"Configure with -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL} "
|
||||
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
|
||||
"check that minSdk instead of adding an override).")
|
||||
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
|
||||
message(STATUS
|
||||
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
|
||||
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
|
||||
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
|
||||
"compile.")
|
||||
endif()
|
||||
|
||||
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
|
||||
|
||||
unset(_mobilegl_android_api)
|
||||
unset(_mobilegl_api_var)
|
||||
unset(_mobilegl_api_triple)
|
||||
endif()
|
||||
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||
|
||||
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
|
||||
# Check if ThinLTO or LTO is suppported
|
||||
include(CheckIPOSupported)
|
||||
include(CheckCCompilerFlag)
|
||||
@@ -185,61 +76,15 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
# Generate MGGitHash.h
|
||||
#
|
||||
# RESULT_VARIABLE, WHICH THIS CALL DID NOT HAVE (CONTRACT-P6 4.2). Without it a failed
|
||||
# `git rev-parse` yields "" and nothing says so. Verified live: building this worktree from WSL
|
||||
# produces an EMPTY hash, because the worktree's `.git` is a file holding a WINDOWS path that
|
||||
# WSL's git cannot resolve - so two builds from different commits got IDENTICAL ABI
|
||||
# fingerprints and the stamp silently stopped doing its documented job.
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE MOBILEGL_GIT_STAMP_RESULT
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT)
|
||||
|
||||
# --- the BUILD STAMP, and why it is a second pair rather than a fix to the first -------------
|
||||
#
|
||||
# GIT_COMMIT_HASH_SHORT MAY NOT MOVE. It is baked into glGetString(GL_VERSION)
|
||||
# (MG_Impl/GLImpl/Getter/GL_Getter.cpp), so redefining it changes an APPLICATION-VISIBLE GL
|
||||
# string and the pull build's .rodata - a behaviour leak and a G1 break at once. The handshake
|
||||
# gets its own pair instead, consumed only by the ABI fingerprint, which no pull build compiles.
|
||||
#
|
||||
# PRESENT IS A FIRST-CLASS INPUT, not the emptiness of VALUE. "this build has no stamp" and
|
||||
# "this build's stamp is the empty string" are different claims, and a fingerprint that could
|
||||
# not tell them apart is how two unrelated builds agreed in the first place.
|
||||
#
|
||||
# -DMOBILEGL_BUILD_STAMP=<anything> (or the environment variable) overrides, because a tree git
|
||||
# cannot read is not always a tree whose commit is unknown - CI has ${GITHUB_SHA} in hand.
|
||||
set(MOBILEGL_BUILD_STAMP "" CACHE STRING "Explicit ABI-fingerprint build stamp when git cannot be read")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "")
|
||||
if(NOT "${MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${MOBILEGL_BUILD_STAMP}")
|
||||
elseif(NOT "$ENV{MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "$ENV{MOBILEGL_BUILD_STAMP}")
|
||||
elseif(MOBILEGL_GIT_STAMP_RESULT EQUAL 0 AND NOT "${GIT_COMMIT_HASH_FULL}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${GIT_COMMIT_HASH_FULL}")
|
||||
endif()
|
||||
|
||||
if("${MOBILEGL_BUILD_STAMP_VALUE}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 0)
|
||||
# A WARNING AND NOT A FATAL: a build with no stamp is still a usable build, it just cannot
|
||||
# prove that a peer was built from the same tree. Saying so once at configure time is the
|
||||
# difference between a known limitation and the silent one 4.2 found.
|
||||
message(WARNING
|
||||
"MobileGL: no build stamp. `git rev-parse HEAD` failed (result "
|
||||
"'${MOBILEGL_GIT_STAMP_RESULT}') and MOBILEGL_BUILD_STAMP was not set, so the ABI "
|
||||
"fingerprint cannot distinguish two builds of the same sizes from different commits - "
|
||||
"which is exactly what a stale MOBILEGL_IPC_SERVER_PATH looks like. Pass "
|
||||
"-DMOBILEGL_BUILD_STAMP=<commit> to restore it.")
|
||||
else()
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 1)
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Util/Miscellany/MGGitHash.h.in
|
||||
${CMAKE_BINARY_DIR}/generated/MGGitHash.h
|
||||
@@ -255,7 +100,6 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
@@ -303,16 +147,11 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Debug/Log.cpp
|
||||
|
||||
MobileGL/MG_Util/Async/JobNode.cpp
|
||||
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
|
||||
|
||||
MobileGL/MG_Util/Math/VectorTypes.cpp
|
||||
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/PipeStats.cpp
|
||||
|
||||
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
|
||||
@@ -341,63 +180,33 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
|
||||
|
||||
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||
@@ -411,10 +220,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
|
||||
@@ -436,7 +242,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||
|
||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||
@@ -472,17 +277,10 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
|
||||
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
|
||||
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
|
||||
@@ -493,180 +291,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MG_Remote (disaggregated transport). Everything below is gated: with the
|
||||
# option OFF not one file here is compiled and no include path is added.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
|
||||
# variable, not a forced cache write, for the reason the two implications below give: an
|
||||
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
|
||||
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
|
||||
# with the transport shadowed off would compile a role-isolation shim over no roles.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
|
||||
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED ON)
|
||||
endif()
|
||||
|
||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||
# a checkout without the submodule must configure and build, just without the
|
||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||
# lines later. Note this only checks for the RUNTIME headers - flatc is never
|
||||
# built here (see scripts/gen_protocol.py).
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
|
||||
message(WARNING
|
||||
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
|
||||
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
|
||||
"disaggregated shape for this configure; the cached ON takes effect once the "
|
||||
"submodule is present.")
|
||||
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
|
||||
# made the plain re-configure after `git submodule update` stay OFF with no message at
|
||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||
# was, so the next configure - with the submodule there - honours it.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
|
||||
# would still be defined over a build with no MG_Remote in it at all.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
|
||||
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
|
||||
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
|
||||
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
|
||||
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
|
||||
# at the CMake level from a legitimate transport-only build, which is why it is stated here
|
||||
# rather than left to whoever hits the link error. Same normal-variable form as the two
|
||||
# implications above.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
|
||||
"this configure: the split path decodes into the MGPipe applier, and the "
|
||||
"applier is what MOBILEGL_PIPE_PUSH compiles")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
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()
|
||||
|
||||
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||
"configure: with nothing pushed it is the only arm there is")
|
||||
set(MOBILEGL_PIPE_LEGACY_MEMOS 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
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Transport/ShmLink.cpp
|
||||
MobileGL/MG_Remote/Transport/LinkMetrics.cpp
|
||||
MobileGL/MG_Remote/Transport/StreamLink.cpp
|
||||
MobileGL/MG_Remote/Transport/Ring.cpp
|
||||
MobileGL/MG_Remote/Transport/Doorbell.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegment.cpp
|
||||
# Both platform halves are listed unconditionally and each is empty on
|
||||
# the other OS, so neither can rot behind an `if (WIN32)` nobody
|
||||
# configures.
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
|
||||
MobileGL/MG_Remote/Transport/FdPassing.cpp
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/SocketTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/ControlInbox.cpp
|
||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||
# ---- P5: the three new directories ------------------------------
|
||||
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
|
||||
# var-tails in and out of SEG_STAGE. [w1]
|
||||
# Client/ the emitting role: session, the 69-slot emit table, the
|
||||
# caps mirror. [c1]
|
||||
# Server/ the applying role: session, the applier bridge onto the
|
||||
# existing MGPipeApply* free functions, the apply thread. [v1]
|
||||
#
|
||||
# Every file below lands in P5 as a HEADER plus a .cpp of named
|
||||
# Fatal stubs, so that all seven P5 packages compile and link on day
|
||||
# one against signatures that cannot then move under them. A stub is
|
||||
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
|
||||
# emitter that returns quietly is how a split lane runs monolith and
|
||||
# goes green (ARCHITECTURE.md 10.3).
|
||||
MobileGL/MG_Remote/CapsCodec.cpp
|
||||
# P6 dl: the one place a session dies - Session::Fail's funnel (Fatal-family aware,
|
||||
# publishes SessionFault, one telemetry point).
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
|
||||
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
|
||||
# P5 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
|
||||
# emitters installed over the two generated tables, which is what stops every
|
||||
# resource/CSO/texture/program record executing synchronously on the GL thread under
|
||||
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
|
||||
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
|
||||
MobileGL/MG_Remote/Client/WireTables.cpp
|
||||
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
MobileGL/MG_Remote/Client/GpuWritePending.cpp
|
||||
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSpawn.cpp
|
||||
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
|
||||
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
|
||||
MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp
|
||||
MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp
|
||||
# P12 (on-screen server window): the process-wide holder of the server's own window
|
||||
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
|
||||
# ServerOwned request is refused by name there).
|
||||
MobileGL/MG_Remote/Server/ServerDisplay.cpp
|
||||
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
|
||||
# arms and the probe that measures the first against the second, shared by the
|
||||
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
|
||||
# so it stays out of the pull build (gate G1).
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -679,16 +303,7 @@ endif()
|
||||
if (ANDROID)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
|
||||
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
|
||||
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Server/DisplayServerJni.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
@@ -698,11 +313,6 @@ if (WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
|
||||
# merged pthread into libc, so it links without asking, but the NDK and musl are not
|
||||
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
set(MOBILEGL_LINK_LIBRARIES
|
||||
glslang::glslang
|
||||
spirv-cross-c
|
||||
@@ -712,59 +322,25 @@ set(MOBILEGL_LINK_LIBRARIES
|
||||
GPUOpen::VulkanMemoryAllocator
|
||||
Vulkan::UtilityHeaders
|
||||
spirv-reflect-static
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
set(MOBILEGL_COMPILE_DEF
|
||||
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
||||
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
||||
-DVMA_VULKAN_VERSION=1001000
|
||||
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
|
||||
# rather than per-target so the shared library and the _s static target agree.
|
||||
-DASIO_STANDALONE
|
||||
-DASIO_NO_DEPRECATED
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/MobileGL
|
||||
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
|
||||
# line above too; this entry lets the client, the backends and MG_Remote spell them
|
||||
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
|
||||
${spirv-tools_SOURCE_DIR}
|
||||
${spirv-tools_SOURCE_DIR}/include
|
||||
${spirv-tools_BINARY_DIR}
|
||||
${SPIRV-Headers_SOURCE_DIR}/include
|
||||
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||
# pimpl so no consumer target needs this path.
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# Header-only runtime: an include path, no add_subdirectory, no link
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
@@ -882,21 +458,8 @@ if (ANDROID)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
|
||||
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
|
||||
# symbols interposes incompatible copies embedded by host libraries such
|
||||
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
|
||||
# visible; GetProcAddress can still return pointers to hidden internals.
|
||||
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
|
||||
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
@@ -904,7 +467,6 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
@@ -960,12 +522,6 @@ if (NOT ANDROID)
|
||||
add_subdirectory(MobileGL/MG_Test)
|
||||
endif()
|
||||
|
||||
# After MG_Test so googletest is already available when the unit tests are
|
||||
# built; the module fetches its own copy when they are not.
|
||||
if (MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_BENCHMARK)
|
||||
add_subdirectory(MobileGL/MG_Benchmark)
|
||||
endif()
|
||||
@@ -974,119 +530,3 @@ if (NOT ANDROID)
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The integration binary is also useful as a standalone adb-shell executable.
|
||||
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
||||
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
|
||||
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
|
||||
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
|
||||
# so a second executable has to be built with an .so name and exec'd out of
|
||||
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
|
||||
# chain end to end: it is packaged like a library, exec'd from the app's own
|
||||
# untrusted_app process, and writes a marker the parent reads back.
|
||||
#
|
||||
# Off by default and ANDROID-only, so no shipping configuration builds it. The
|
||||
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
# P6 `sm`: the REAL server image. Unlike MOBILEGL_BUILD_SERVER_SPIKE below - the
|
||||
# P0 spike, which links no MobileGL library and exists only to prove the Android
|
||||
# delivery chain - this one links the library and runs mobilegl_server_main.
|
||||
#
|
||||
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
|
||||
# dlsym's the entry point, so one shared library serves both roles. On a desktop
|
||||
# configure we link it directly instead: there is no APK packaging rule to satisfy
|
||||
# and a direct link is what makes an undefined symbol a BUILD error rather than a
|
||||
# runtime dlsym failure in a child whose stderr is /dev/null.
|
||||
#
|
||||
# Android keeps the lib*.so naming and the nativeLibraryDir output the spike
|
||||
# proved is the only exec-able location in an APK.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT WIN32)
|
||||
add_executable(MobileGLServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Remote/Server/ServerEntry.cpp)
|
||||
|
||||
target_include_directories(MobileGLServer PRIVATE
|
||||
${MOBILEGL_INCLUDE_DIR})
|
||||
|
||||
target_link_libraries(MobileGLServer PRIVATE ${CMAKE_PROJECT_NAME})
|
||||
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServer PRIVATE -pie)
|
||||
|
||||
# $ORIGIN, OR THE SERVER CANNOT START AT ALL ON ANDROID.
|
||||
#
|
||||
# This executable links libMobileGL.so, and it is EXEC'd rather than dlopen'd - so it gets a
|
||||
# fresh linker namespace with the default search path, not the app's. An APK's lib/<abi>/ is
|
||||
# not on that path, and the failure is the bluntest one there is:
|
||||
#
|
||||
# F linker: CANNOT LINK EXECUTABLE ".../lib/arm64/libMobileGLServer.so":
|
||||
# library "libMobileGL.so" not found: needed by main executable
|
||||
#
|
||||
# It cost a device run to find because it is invisible on desktop: CMake gives build-tree
|
||||
# targets an absolute RPATH automatically, so the WSL server resolved its dependency through a
|
||||
# path that does not exist on a phone. $ORIGIN says "beside me", which is true in the APK, true
|
||||
# in the build tree, and true wherever somebody copies the pair - and it is what makes a
|
||||
# deployment that is NOT a build tree work at all, which the end state is.
|
||||
# THE PROPERTIES, NOT target_link_options. A raw `-Wl,-rpath,$ORIGIN` reaches the generator
|
||||
# unquoted and the SHELL expands $ORIGIN to nothing - measured on device, where the linker
|
||||
# said `normalize_path - invalid input: "$"` and then failed to find libMobileGL.so anyway.
|
||||
# BUILD_RPATH / INSTALL_RPATH are the supported spelling and CMake escapes them correctly.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
|
||||
if (ANDROID)
|
||||
# An APK's only exec-able install location is lib/<abi>/, and the packager
|
||||
# only puts a file there if it is named lib*.so. P0 spike A proved this
|
||||
# works from the app's own untrusted_app domain on two devices.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer")
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
else()
|
||||
# Desktop: the name the dladdr fallback looks for beside libMobileGL.so.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
# Renamed by P6 sm: the real server took the name it had been holding.
|
||||
add_executable(MobileGLServerSpike
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
|
||||
|
||||
# An executable that is named like a shared library still has to be a real
|
||||
# PIE executable: Android has refused non-PIE executables since API 21, and
|
||||
# the name alone does not change what the loader demands of the file.
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServerSpike"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServerSpike PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServerSpike PRIVATE -pie)
|
||||
|
||||
# AGP packages what the external native build drops into the per-ABI output
|
||||
# directory, and it selects by the .so extension. CMake puts executables in
|
||||
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
|
||||
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
|
||||
# the library directory when the generator gave us one.
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+20
-617
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -29,33 +29,6 @@ namespace MobileGL::MG_Config {
|
||||
ForceOff,
|
||||
};
|
||||
|
||||
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
|
||||
// never a demand: the renderer clamps it to what the device supports at device
|
||||
// creation, falling down the chain ext -> indirect -> unroll with one log line.
|
||||
enum class MultiDrawMode : Uint8 {
|
||||
Auto = 0, // unset: best supported tier
|
||||
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
|
||||
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
|
||||
Unroll, // one vkCmdDraw* per sub-draw
|
||||
};
|
||||
|
||||
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
|
||||
// such entry point in core, so every tier below is an emulation; they differ only in
|
||||
// which driver capability they lean on and how many driver calls a batch costs. Like
|
||||
// the Magma knob this is a preference, clamped at resolution time to what the ES
|
||||
// driver actually supports, with one log line when it falls back.
|
||||
enum class GLESMultiDrawMode : Uint8 {
|
||||
Auto = 0, // unset: best supported tier
|
||||
Ext, // one glMultiDrawElementsBaseVertexEXT
|
||||
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
|
||||
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
|
||||
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
|
||||
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
|
||||
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
|
||||
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
|
||||
// drawn by a single glDrawElements
|
||||
};
|
||||
|
||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||
@@ -69,102 +42,21 @@ namespace MobileGL::MG_Config {
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EsprytEnableTextureView = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool EsprytUseAngle = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool MagmaDisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
// bounds. The pass grows that one array to what the device's topology needs and
|
||||
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||
// verbatim.
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
|
||||
// Off by default so an application that checks the string before using doubles keeps
|
||||
// its float path; on for measuring what the conformance suite makes of the demoted
|
||||
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
|
||||
Bool AdvertiseFp64 = false;
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool EsprytAvoidExplicitLodBias = false;
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
|
||||
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
|
||||
// match them by block name and member sequence instead. The Mali ES driver delivers
|
||||
// nothing at all through a located block once a tessellation or geometry stage is in
|
||||
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
|
||||
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
|
||||
// healthy driver - which is what the integration lane does, since llvmpipe and
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -174,51 +66,25 @@ namespace MobileGL::MG_Config {
|
||||
Bool CoherentAsFlush = false;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
Bool TraceSkipAutodestroy = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool EsprytDisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
// glTexSubImage from the client pointer instead of staging them through the
|
||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||
// hatch).
|
||||
Bool EsprytDisableUnpackRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
|
||||
// (glBufferSubData / map flushes) back to the immediate driver upload instead
|
||||
// of queueing them for the staged-copy flush through the persistent-mapped
|
||||
// upload ring (negative control / driver-bug escape hatch; the immediate
|
||||
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
|
||||
Bool EsprytDisableUploadRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
|
||||
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
|
||||
// to the upload ring's staged glCopyBufferSubData (negative control / escape
|
||||
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
|
||||
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
// Adreno does not), which means the emulation is dead code on exactly the stack the
|
||||
// headless suite runs on. This forces it live so the scenarios and the CTS can
|
||||
// exercise the path, and gives the device an A/B lever over the same choice.
|
||||
Bool EsprytForceDepthStencilReadbackEmulation = false;
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
@@ -226,473 +92,10 @@ namespace MobileGL::MG_Config {
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool MagmaDisableRobustBufferAccess = false;
|
||||
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
|
||||
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
|
||||
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
|
||||
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
|
||||
// supported tier, which never includes "compute" - see the note on its resolution.
|
||||
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
|
||||
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
|
||||
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
|
||||
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
|
||||
// both the threading and the application-visible behaviour change.
|
||||
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
||||
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||
Uint32 AsyncShaderCompileThreads = 0;
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
|
||||
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
|
||||
// empty, WITHOUT joining the job (latched per compile - see
|
||||
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
|
||||
// a real failure still fails the program link with the compile log quoted. It
|
||||
// exists for applications that compile hundreds of shaders serially and read the
|
||||
// status right after each glCompileShader - Iris's shader-pack load - where those
|
||||
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
|
||||
// phase issues no program-level query between programs; program-level LINK_STATUS
|
||||
// and the program info log still join truthfully, so paths that check each link
|
||||
// immediately stay serial by their own construction). Off by default; never
|
||||
// advertise it.
|
||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
|
||||
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
|
||||
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
|
||||
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
|
||||
// ON; ForceOff turns ALL THREE off and makes every translation run from
|
||||
// scratch. The escape hatch exists because a wrong cache hit is a silently
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
// it is ON even where the driver advertises GL_OES_viewport_array, because that
|
||||
// extension only ever gave the SHADER a compilable name: MobileGL has never
|
||||
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
|
||||
// and nothing else), so on an extension-capable driver every index rasterized as
|
||||
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
|
||||
// the pre-emulation path, extension passthrough where it exists and
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
|
||||
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
|
||||
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
|
||||
// packed16 allocations with a MIRRORED field order (allocation-scoped and
|
||||
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
|
||||
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
|
||||
// reversed whenever exactly one endpoint sits in a mirrored allocation
|
||||
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
|
||||
// With no 16-bit packed ES image left there is no field order to disagree about; the
|
||||
// client word still round-trips exactly, because the canonical shadow is already
|
||||
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
|
||||
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
|
||||
// path); ForceOff keeps the native narrow storage even where the probe fires - the
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||
// the same capture-less draws and arms the best proven one: the dedicated
|
||||
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
|
||||
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
|
||||
// is the "everything before P5e" control P5e's A/B is run against - each phase's
|
||||
// constant survives as the next phase's control, which is why none of them is ever
|
||||
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
|
||||
// bits are listed here (MG_Pipe/MGPipe.h owns them):
|
||||
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
||||
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||
// set_texture_params) - requires 0x80 AND 0x800
|
||||
// (the built-in sampler CSO a set_texture_params record names is minted by
|
||||
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
|
||||
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
|
||||
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
|
||||
// classes and the dirty bits 15/16/17) - requires 0x80
|
||||
// (every MGPBufferRange::Res names a Buffer handle; P5e)
|
||||
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
|
||||
// family runs its legacy arm; it is never half-run.
|
||||
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||
// never probed. The negative control the CSO design is measured against.
|
||||
Uint64 PipePush = 0;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
// counts instead, for triage and for the lane that must survive to read its own
|
||||
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
|
||||
// off.
|
||||
Bool PipeVerifyFatal = true;
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
|
||||
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
|
||||
// green verify run goes red naming it (negative control A). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipeVerifyCorrupt;
|
||||
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
|
||||
// the value) of that field for that verb, an omission indistinguishable from a
|
||||
// forgotten FillPoints.def row, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
// per-draw staging, counted separately in the stats.
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
// the log whenever PipeStats is on, so a device run needs no writable path.
|
||||
String PipeStatsFile;
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
|
||||
//
|
||||
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
|
||||
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
|
||||
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
|
||||
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
|
||||
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
|
||||
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
|
||||
// behind their own #if.
|
||||
//
|
||||
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
|
||||
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
|
||||
// parser below does not exist to complain about it, and putting a complaint in the
|
||||
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
|
||||
// shape of "the split lane ran monolith and went green", so the gate against it is a
|
||||
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
|
||||
// P5 adds beside build-linux-verify).
|
||||
enum class TransportMode : Uint8 {
|
||||
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
|
||||
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
|
||||
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
|
||||
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
|
||||
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
|
||||
// transport in is not the same as using it, and every existing lane of a build-split
|
||||
// must keep running monolith unless it is asked for one.
|
||||
extern TransportMode Transport;
|
||||
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
|
||||
extern String TransportEndpoint;
|
||||
|
||||
// P7 F1. DID **THIS PROCESS'S OWN CONFIGURATION** ASK FOR A SPLIT TRANSPORT.
|
||||
//
|
||||
// `Transport` alone cannot answer that, and the difference is exactly the one the F1 gate
|
||||
// turns on. A unit fixture assigns `Transport` by hand to put the code under test on its
|
||||
// split arm (ServerLoopTest's main does, and says why) while building no client at all; a
|
||||
// real run gets it from MG_ConfigLoader::Init(), and that run WILL bring a client half up.
|
||||
// Only the second may be held to "never decide a record family's fate from a placeholder
|
||||
// caps mirror" - in the first there is no handshake, no mirror to adopt and nothing the
|
||||
// rule could mean.
|
||||
//
|
||||
// Written once, by InitTransport(), and never cleared: a process does not stop having been
|
||||
// configured. Read by MG_Remote::Client::CapsMirror::RequireFirstSnapshot.
|
||||
extern Bool SplitTransportRequestedByConfig;
|
||||
|
||||
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
|
||||
// for the G1 reason above and because every field here is meaningless without the
|
||||
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
|
||||
//
|
||||
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
|
||||
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
|
||||
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
|
||||
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
|
||||
// to P6 or later.
|
||||
//
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE's two values. See IpcTable::Surface.
|
||||
enum class IpcSurface : Uint8 {
|
||||
Offscreen = 0, // a window surface is the client's own window, as before (the default)
|
||||
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
|
||||
};
|
||||
struct IpcTable {
|
||||
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
|
||||
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
|
||||
// SPLIT variant already carry it, and an environment variable that nothing parses is
|
||||
// indistinguishable from one that is parsed and ignored.
|
||||
String ServerPath;
|
||||
// Control endpoint and data-plane selection are independent of topology.
|
||||
String Control = "fork"; // fork | unix:<path> | tcp://host:port
|
||||
String Data = "auto"; // auto | shm | stream
|
||||
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
|
||||
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
|
||||
// codec publish a max-record-bytes counter rather than assume that is enough.
|
||||
Uint32 RingMb = 8;
|
||||
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
|
||||
// here ONE BLOB AT A TIME: a row whose content can outgrow the segment cuts it at the
|
||||
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
|
||||
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
|
||||
Uint32 StageMb = 32;
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
|
||||
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
|
||||
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
|
||||
// Stores no command names are destroyed at once and stores whose last submission has
|
||||
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
|
||||
// exceed it after a sweep, the server flushes what it has recorded and waits for it
|
||||
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
|
||||
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
|
||||
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
|
||||
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
|
||||
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
|
||||
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
|
||||
// and MagmaWireReclaimScenario's watermark cases must go red.
|
||||
Uint32 WireDeferredMb = 64;
|
||||
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
|
||||
Uint32 SpinUs = 50;
|
||||
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
|
||||
// reverse-channel event that finds SEG_EVENT full under run-ahead. The apply thread
|
||||
// publishes, rings the client and parks until the client drains enough room; a client
|
||||
// that has not made room when this runs out - it stopped draining (NotDraining) or it
|
||||
// drains a slot at a time (TooSlow) - FORFEITS the reverse channel: that event and every
|
||||
// later one is dropped and counted (eventDropped), and the session stops by the ordinary
|
||||
// stop path rather than by Fatal{EventRingOverflow}. A peer that goes away (PeerGone) or
|
||||
// a session that is stopped (Stopped) ends the wait at once instead, so the knob may
|
||||
// exceed ServerLoop::Stop()'s 5000 ms join. It bounds the whole reservation, not one
|
||||
// park, so a trickling peer cannot stretch it - nor can a shm peer that writes the
|
||||
// (peer-writable) eventRingFull latch to 0 itself, though that one keeps the apply
|
||||
// thread spinning rather than parked until the budget runs out. Was a 30000 ms constant
|
||||
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
|
||||
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
|
||||
Uint32 EventWaitMs = 2000;
|
||||
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||
Uint32 PersistentBlockKb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
|
||||
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
|
||||
// range every verb. 0 restores the whole-range push (A/B control).
|
||||
Uint32 PersistentHashSuppress = 1;
|
||||
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
|
||||
// with no reply slot) are published without waiting for their own apply; the barrier
|
||||
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
|
||||
// every reply-slot row), which is the only place BARRIER-PULLED fields are read. 0
|
||||
// restores the per-record barrier of R-1.
|
||||
Uint32 BatchWaits = 1;
|
||||
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
|
||||
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
|
||||
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
|
||||
// answer is taken when the window fills.
|
||||
//
|
||||
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
|
||||
// measured reasons and one structural one, in the order they were found:
|
||||
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
|
||||
// client emits on the order of a thousand records - so the answer was always gone
|
||||
// (`want=310` on every miss while the ring spanned [1175..1504]);
|
||||
// 2. a residency budget that tried to take answers back early could not help, because
|
||||
// what displaces an answer is what the SERVER sends, not what the client emits;
|
||||
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
|
||||
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
|
||||
//
|
||||
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
|
||||
// frame-sent identical per op throughout):
|
||||
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
|
||||
// window 2 2,500 17.27 s
|
||||
// window 4 1,644 11.02 s
|
||||
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
|
||||
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
|
||||
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
|
||||
//
|
||||
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
|
||||
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
|
||||
// now; the old 4 was derived from a reply pool the window does not run on). Three
|
||||
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
|
||||
//
|
||||
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
|
||||
// window 5 1,277 11.31 s (10.61-12.12)
|
||||
// window 6 1,155 10.79 s ( 9.69-10.81)
|
||||
// window 8 859 10.96 s ( 8.92-11.03)
|
||||
//
|
||||
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
|
||||
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
|
||||
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
|
||||
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
|
||||
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
|
||||
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
|
||||
// the constant ~350-400 creates that block for a reason depth does not reach, and the
|
||||
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
|
||||
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
|
||||
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
|
||||
// flag keyed on wantReply would disable the very mechanism it exists to enable
|
||||
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
|
||||
Uint32 CreateWindow = 10;
|
||||
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||
// negative control has a spelling the day P11 writes it.
|
||||
Uint32 AdoptTier = 2;
|
||||
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
|
||||
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
|
||||
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
|
||||
// GLContext by the client's residual fill and a free-running queue lets the server
|
||||
// read a FUTURE value of them.
|
||||
Uint32 VerbBarrier = 1;
|
||||
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
|
||||
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
|
||||
// It is one half of a conjunction and never a switch on its own - the client arms
|
||||
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
|
||||
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
|
||||
// saying so.
|
||||
//
|
||||
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
|
||||
// every record are identical on both arms, and the only difference is whether the
|
||||
// client waits. That is what makes "is the picture the same" a question about the wait
|
||||
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
|
||||
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
|
||||
// the first barriered row's stale pull.
|
||||
//
|
||||
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
|
||||
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
|
||||
// arm that stops filling it.
|
||||
Uint32 RunAhead = 1;
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
|
||||
// have in flight before it waits for a swap to come back. 1 = the client publishes
|
||||
// frame N+1's records while the server applies and swaps frame N - one frame of
|
||||
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
|
||||
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
|
||||
// client that is already CPU-bound and costs a frame of latency, which is why the
|
||||
// default is 1 and not "as deep as the ring".
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
|
||||
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
|
||||
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
|
||||
// latch decides dead (device lost) from alive-but-silent (a named diagnostic). The
|
||||
// contract named this knob from P6 on; the client hard-coded its default until P7.
|
||||
Uint32 ControlTimeoutMs = 5000;
|
||||
// MOBILEGL_IPC_COLD_START_MS (P7): the same wait for the three ops a server may bring its
|
||||
// NATIVE backend up inside - CreatePbufferSurface, CreateWindowSurface, MakeCurrent -
|
||||
// until the session's first MakeCurrent is answered ok. The bring-up is lazy (Espryt's
|
||||
// eglInitialize, Magma's Vulkan instance and device), ~100 ms on a workstation and more
|
||||
// than the steady 5 s on a loaded CI runner (retrace-split spawn legs, runs 35671704873
|
||||
// and 35706183230). Never shorter than CONTROL_TIMEOUT_MS; its expiry is the same
|
||||
// non-fatal named answer. The default is the spawn connect budget, for the same reason:
|
||||
// "the server has to create a backend, and a cold software rasteriser is not fast".
|
||||
Uint32 ColdStartMs = 20000;
|
||||
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
|
||||
// build, the seven sticky forwards that are otherwise exempt - from "count it in
|
||||
// rsp" to Fatal (R-7.3).
|
||||
Bool StrictErrors = false;
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
|
||||
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
|
||||
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
|
||||
// only because the two roles share one object turns into a named
|
||||
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
|
||||
// Meaningless under monolith transport (the two roles are one thread there, so the
|
||||
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
|
||||
// (the comparator owns the one fill block it compares against). 0 is not merely the
|
||||
// default, it is the negative control: the lane's distinctness case must go red
|
||||
// without it (P5F §6).
|
||||
Bool RoleSplitState = false;
|
||||
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||
Bool Audit = false;
|
||||
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
|
||||
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
|
||||
// because the resolved mask is logged by whoever starts the apply thread, and the
|
||||
// string is what an operator typed.
|
||||
String ServerAffinity = "auto";
|
||||
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
|
||||
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
|
||||
// any native window including NULL, send ONE CreateWindowSurface naming
|
||||
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
|
||||
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
|
||||
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
|
||||
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
|
||||
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
|
||||
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
|
||||
IpcSurface Surface = IpcSurface::Offscreen;
|
||||
};
|
||||
extern IpcTable Ipc;
|
||||
|
||||
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
|
||||
// surface is the server's when the knob says so AND there is a remote server to own it.
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+8
-405
@@ -7,20 +7,9 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Config.h"
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
|
||||
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||
// pull build's translation unit is unchanged.
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
|
||||
// unchanged (G1).
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
@@ -30,16 +19,6 @@ namespace MobileGL::MG_Config {
|
||||
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
|
||||
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
|
||||
FeaturesTable Features;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
|
||||
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
|
||||
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
|
||||
// and there is nothing to define.
|
||||
TransportMode Transport = TransportMode::Monolith;
|
||||
String TransportEndpoint;
|
||||
Bool SplitTransportRequestedByConfig = false;
|
||||
IpcTable Ipc;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
namespace MobileGL::MG_ConfigLoader {
|
||||
@@ -118,47 +97,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
: MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
|
||||
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
|
||||
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return MG_Config::MultiDrawMode::Auto;
|
||||
}
|
||||
String lowered = it->second;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
|
||||
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
|
||||
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
|
||||
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
|
||||
key.c_str(), it->second.c_str());
|
||||
return MG_Config::MultiDrawMode::Auto;
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
|
||||
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return MG_Config::GLESMultiDrawMode::Auto;
|
||||
}
|
||||
String lowered = it->second;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
|
||||
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
|
||||
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
|
||||
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
|
||||
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
|
||||
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
|
||||
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
|
||||
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
|
||||
key.c_str(), it->second.c_str());
|
||||
return MG_Config::GLESMultiDrawMode::Auto;
|
||||
}
|
||||
|
||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
@@ -180,120 +118,27 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return static_cast<Uint32>(parsedValue);
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
|
||||
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
|
||||
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
|
||||
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
|
||||
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
|
||||
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const String& value = it->second;
|
||||
const char* text = value.c_str();
|
||||
int base = 10;
|
||||
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
text += 2;
|
||||
base = 16;
|
||||
}
|
||||
char* parseEnd = nullptr;
|
||||
errno = 0;
|
||||
const bool negative = value.find('-') != String::npos;
|
||||
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
|
||||
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
|
||||
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
|
||||
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return static_cast<Uint64>(parsedValue);
|
||||
}
|
||||
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||
features.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.EsprytViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
|
||||
#else
|
||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||
// "pull everything" and that stays literally true.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||
#endif
|
||||
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, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
@@ -312,254 +157,12 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
#undef ENTRY
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
|
||||
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
|
||||
// one fallback that names what it did instead. The two prefixed forms are the only
|
||||
// reason this is not literally that function's ENTRY macro.
|
||||
//
|
||||
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
|
||||
// rather than silent, because the failure this avoids is a P6 lane that set
|
||||
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
|
||||
// The mode is left at Monolith so nothing half-initializes.
|
||||
inline void InitTransport() {
|
||||
String value;
|
||||
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||
String lowered = value;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
MG_Config::TransportEndpoint.clear();
|
||||
if (lowered.empty() || lowered == "monolith") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
if (lowered == "inproc") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
|
||||
// one, and NOT on the two that name a transport and then stay monolith - a process
|
||||
// that was refused its transport brings no client half up either.
|
||||
MG_Config::SplitTransportRequestedByConfig = true;
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread");
|
||||
return;
|
||||
}
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||
// rather than "unknown", which are different bugs on the operator's side.
|
||||
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
|
||||
// rendezvous of its own and connects to it - the two processes are
|
||||
// independent, so this is `Dial = Connect` with a launcher attached
|
||||
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
|
||||
if (lowered == "spawn") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Spawn;
|
||||
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
|
||||
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
|
||||
// run_trace_case.cmake asserts on a distinctive sentence from this
|
||||
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
|
||||
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
|
||||
// every MOBILEGL_* in EVERY build including the pull one - so the
|
||||
// bare KEY=VALUE is satisfied by a monolith library that parsed
|
||||
// nothing (review M-5). A spawn arm with no line of its own could
|
||||
// only have been asserted with the inproc marker, which spawn never
|
||||
// emits: the arm would have been red for a missing sentence rather
|
||||
// than green for a working transport.
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread in ANOTHER PROCESS");
|
||||
return;
|
||||
}
|
||||
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
|
||||
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
|
||||
// ELSE is listening on needs the server lifecycle to be somebody
|
||||
// else's too, which is P12's. Named, never a silent degrade.
|
||||
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
|
||||
"staying on monolith. This run is NOT a split run.",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
|
||||
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
|
||||
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
|
||||
// how an A/B stops being one, and the ranges below are the diagnostics.
|
||||
inline void InitIpc() {
|
||||
auto& ipc = MG_Config::Ipc;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
|
||||
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
|
||||
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
|
||||
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
|
||||
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
|
||||
// create_shader_state archive already needs far more than a toy ring. The ceilings
|
||||
// are sanity, not policy.
|
||||
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
|
||||
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
|
||||
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
|
||||
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
|
||||
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
|
||||
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
|
||||
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
|
||||
// patience at all would forfeit a healthy run-ahead client the first time its ring
|
||||
// filled between two of the client's drains, which is an ordinary backlog.
|
||||
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
|
||||
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||
// Whole-range push is the 0 arm; with it on (default) only blocks whose
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
|
||||
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
|
||||
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
|
||||
// was inert before, the device numbers, and the sweep that put the default at 6 rather
|
||||
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
|
||||
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
|
||||
// declared answers, and the device sweep showed the wall clock still falling at the old
|
||||
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
|
||||
// StreamLink's crossing log are the checks on this number.
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
// shadow comparer is armed.
|
||||
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
|
||||
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
|
||||
// point of use, which is where the "P11" in the message belongs.
|
||||
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
|
||||
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
|
||||
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
|
||||
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
|
||||
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
|
||||
// run-ahead's whole point is that the client stops filling gPipeInputs for an
|
||||
// unbarriered record, and the comparator has nothing left to compare when it does.
|
||||
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
|
||||
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
|
||||
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
|
||||
// admitted: a credit of zero would mean "publish no present at all".
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
|
||||
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
|
||||
// Both are floored at 100 ms: a zero bound would declare every live server silent.
|
||||
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
|
||||
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
|
||||
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
|
||||
// verify harness: the comparator's entry compare and compare-at-read hook are built on
|
||||
// there being ONE filled block (the hook pins itself to &gPipeInputs,
|
||||
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
|
||||
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
|
||||
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
|
||||
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
|
||||
"the dual-block rehearsal is OFF for this run");
|
||||
ipc.RoleSplitState = false;
|
||||
}
|
||||
|
||||
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
|
||||
//
|
||||
// The device-lost latch is deliberately one-way - a session whose server died has
|
||||
// nothing to recover into, because every handle the client minted names an object in a
|
||||
// process that no longer exists. Re-pushing the world onto a fresh server is the work
|
||||
// this knob would turn on, and no stage has written it.
|
||||
//
|
||||
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
|
||||
// the whole IPC family: an environment variable nothing consumes is indistinguishable
|
||||
// from one that is consumed and does nothing, and an operator who set this one would
|
||||
// otherwise conclude that recovery had been tried and had not helped.
|
||||
{
|
||||
String respawn;
|
||||
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
|
||||
if (!respawn.empty() && respawn != "0") {
|
||||
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
|
||||
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
|
||||
"this client minted names an object inside the server process, so a new "
|
||||
"server would have to be re-pushed the entire world before a single verb "
|
||||
"could land. This run will latch device-lost and stay there.",
|
||||
respawn.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
|
||||
// the semantics). An unknown value is named and read as the default, like every knob here.
|
||||
{
|
||||
String surface;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
|
||||
std::transform(surface.begin(), surface.end(), surface.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (surface.empty() || surface == "offscreen") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
// (Not said by a server process: a spawned server inherits the client's environment,
|
||||
// parses this before RunSession sets its own transport, and has no client half anyway.)
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
|
||||
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// Said, not silently honoured: there is no remote server here to own a window.
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
|
||||
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
|
||||
"and this run has none. Window surfaces stay the client's own");
|
||||
}
|
||||
}
|
||||
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// One line, on the arm where these numbers decide behaviour, because every one of
|
||||
// them is a number a bug report has to quote.
|
||||
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
|
||||
// missing while the value was a number nobody varied, and the moment it became one that
|
||||
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
|
||||
// log that does not name it makes every run ambiguous about its own configuration.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
|
||||
"persistent-block=%uKiB create-window=%u "
|
||||
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
|
||||
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.AdoptTier,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
|
||||
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
|
||||
if (ipc.VerbBarrier == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
|
||||
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
|
||||
"live GLContext, so an unbarriered queue lets the server read future values");
|
||||
}
|
||||
if (ipc.WireDeferredMb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
|
||||
"server never forces a sync for orphaned wire buffer stores, so a long frame "
|
||||
"that respecifies and draws holds every one of them until the frame ends");
|
||||
}
|
||||
if (ipc.PersistentBlockKb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
|
||||
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Loading configuration from environment variables...");
|
||||
InitializeAcceptedEnvVariables();
|
||||
|
||||
InitBackendType();
|
||||
InitFeatures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
|
||||
// configuration, and before the accepted-env map is destroyed just below.
|
||||
InitTransport();
|
||||
InitIpc();
|
||||
#endif
|
||||
|
||||
// Destroy the map since we won't need it anymore
|
||||
acceptedEnvVariablesMap.reset();
|
||||
|
||||
+4
-37
@@ -9,20 +9,10 @@
|
||||
#pragma once
|
||||
|
||||
// ============== Platform-specific definitions and macros ============== //
|
||||
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
|
||||
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
|
||||
// by the configure-time guard in CMakeLists.txt), not by a macro.
|
||||
//
|
||||
// History: this used to `#define __ANDROID_API__ 26` to *raise* the level back
|
||||
// when the build configured something lower, so that pthread_getname_np (which
|
||||
// bionic guards with __INTRODUCED_IN(26)) would be declared. Once a later
|
||||
// change added an `#undef` in front of it, the same line started *lowering* the
|
||||
// level whenever the build configured higher than 26 - and that is an
|
||||
// include-order split-brain, not a compatibility knob: a TU that includes any
|
||||
// libc++ header before Includes.h latches libc++'s feature macros at the
|
||||
// configure-time level, and only the bionic headers pulled in afterwards see
|
||||
// the lowered value. The two halves then disagree (e.g. libc++ believes
|
||||
// pthread_cond_clockwait exists while bionic has since hidden its declaration).
|
||||
#ifdef __ANDROID__
|
||||
#undef __ANDROID_API__
|
||||
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
@@ -47,23 +37,6 @@
|
||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||
|
||||
// ====================== MobileGL configurations ======================= //
|
||||
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
|
||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
||||
//
|
||||
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
|
||||
// drops only D. Any edit here must be mirrored in Log.h.
|
||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
#endif
|
||||
|
||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
||||
#endif
|
||||
@@ -95,12 +68,6 @@
|
||||
#endif
|
||||
|
||||
// =============================== Utils ================================ //
|
||||
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
|
||||
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
|
||||
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
|
||||
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
|
||||
// 2026-08-13 renumbering unchanged; the contract is and stays
|
||||
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@
|
||||
#include <stacktrace>
|
||||
#endif
|
||||
|
||||
// Include ska::flat_hash_map
|
||||
#include <ska/flat_hash_map.hpp>
|
||||
// Include FastSTL
|
||||
#include <FastSTL/UnorderedMap.h>
|
||||
|
||||
// Include xxHash
|
||||
#include <xxhash.h>
|
||||
|
||||
+6
-132
@@ -14,16 +14,6 @@
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
@@ -46,70 +36,14 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
|
||||
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
|
||||
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
|
||||
// symbol and no resized one (the first version called it unconditionally and moved
|
||||
// DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than
|
||||
// the call is the load-bearing part.
|
||||
MG_Backend::ShutdownSplitRoles();
|
||||
#endif
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
// off, and idempotent.
|
||||
MG_Util::PipeStats::Shutdown();
|
||||
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||
// both of which this function is about to destroy. This is the one
|
||||
// cancellation path in the whole design that waits.
|
||||
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
||||
// GL syncs die with their contexts, and every context is gone by the
|
||||
// time full teardown runs: drain the live-sync registry while the
|
||||
// backend function table can still release the backend handles (and
|
||||
// before a re-initialized library could pair them with the wrong
|
||||
// backend's DeleteSync).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
// Queries die with their contexts for the same reason, and their registry
|
||||
// is the same shape of process-global map: drain it here too, while the
|
||||
// function table can still pair each backend handle with the backend that
|
||||
// minted it.
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
glslang::FinalizeProcess();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
|
||||
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
|
||||
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
|
||||
// glslang's process-wide pool allocator and every cached built-in symbol table,
|
||||
// while the TShader/TProgram objects owned by the shader and program objects
|
||||
// still reference levels adopted from those tables. Finalizing first left live
|
||||
// glslang objects pointing at freed memory for the rest of the teardown.
|
||||
glslang::FinalizeProcess();
|
||||
// Immediately after, and never apart from it: FinalizeProcess just deleted the
|
||||
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||
// The two-level translation memo. Nothing in it references a glslang object -
|
||||
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
|
||||
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
|
||||
// fordebug build gets one line per level saying how the run went.
|
||||
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
|
||||
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
|
||||
MG_State::GLState::LogProgramTranslationCacheStats();
|
||||
MG_State::GLState::ClearProgramTranslationCache();
|
||||
MG_Backend::gBackendFunctionsTable = {};
|
||||
g_isInitialized = false;
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
|
||||
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
|
||||
// FATAL=0 summary the comparator used to write from __run_exit_handlers truncated the
|
||||
// client half of every such run to that one line. Verify builds only - the guard is
|
||||
// the compile definition, not a runtime knob, and the pull build gains no byte (G1).
|
||||
MG_Pipe::MGPipeVerifyFlushSummary();
|
||||
#endif
|
||||
if (logLifecycle) {
|
||||
MG_Util::Debug::Close();
|
||||
}
|
||||
@@ -128,71 +62,13 @@ namespace MobileGL {
|
||||
MGLOG_I("Initializing MobileGL...");
|
||||
MG_ConfigLoader::Init();
|
||||
MGLOG_I("Config loaded");
|
||||
// Immediately after the config load and before anything can count: the MGPipe
|
||||
// boundary counters latch their enable flag here, so every counting site in the
|
||||
// two backends is a load of an already-settled global for the rest of the run.
|
||||
MG_Util::PipeStats::Init();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// F1 (P7 wave 2). THE WIRE COMES UP BEFORE THE FIRST FRONTEND OBJECT IS BORN, and the
|
||||
// ORDER is the fix - not a wait, and not a friendlier default mask.
|
||||
//
|
||||
// MG_State::Init() constructs GLContext, and GLContext's TextureState constructor
|
||||
// materialises one default texture object per target (GL 3.3 core 3.8,
|
||||
// TextureState.cpp:62-71). TextureObjectBase's constructor emits resource_create for
|
||||
// every one of them (TextureState/TextureObject.cpp:168-169), and that emission is
|
||||
// gated, through PipeFill's FamilyIsLive, on
|
||||
// CapsMirror::ServerConsumes(kMGPipeSubsystemTextureResources). In the old order the
|
||||
// mirror was still GENERATION 0 when it was asked - it had no snapshot because the
|
||||
// session had not been started yet, MG_Backend::Init() being what starts it - so it
|
||||
// answered from the placeholder, every one of those creates was withheld, and the log
|
||||
// said so once and then never again:
|
||||
//
|
||||
// MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
|
||||
// family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
|
||||
// caps generation 0
|
||||
//
|
||||
// That is not a race and it never was one: the line reproduces byte-for-byte on
|
||||
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
|
||||
// thread. And "the legacy pull path runs for it" is false under split - the client has
|
||||
// no pull path to fall back to - so the records are simply lost, and only a later
|
||||
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
|
||||
//
|
||||
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
|
||||
// MG_State::Init() no handshake is in flight, and the one that will land is started
|
||||
// LATER BY THIS SAME THREAD. The only correct answer is to have already brought it up,
|
||||
// which is what this hook does. ServerConsumes now REFUSES to answer from a
|
||||
// placeholder (CapsMirror.cpp), so a regression of this order is a named Fatal rather
|
||||
// than a whole family of silently dropped records.
|
||||
//
|
||||
// In a build without MOBILEGL_BUILD_DISAGGREGATED this statement does not exist, so
|
||||
// the pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
const Bool splitBackendIsUp = MG_Backend::InitSplitRolesBeforeState();
|
||||
#endif
|
||||
MG_State::Init();
|
||||
MGLOG_D("MG_State initialized");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!splitBackendIsUp)
|
||||
#endif
|
||||
{
|
||||
MG_Backend::Init();
|
||||
}
|
||||
MG_Backend::Init();
|
||||
MGLOG_D("MG_Backend initialized");
|
||||
MG_Impl::Init();
|
||||
MGLOG_D("MG_Impl initialized");
|
||||
glslang::InitializeProcess();
|
||||
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
|
||||
// tables lazily under a process-wide lock held for the whole build, so without this
|
||||
// the first concurrent compiles of a shaderpack all serialize behind the very first
|
||||
// parse and asynchronous compilation looks like it is doing nothing.
|
||||
//
|
||||
// Gated on the flag, because the problem it solves only exists when there are
|
||||
// workers: with compilation synchronous, nothing ever contends for that lock and the
|
||||
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
|
||||
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
|
||||
// on the async subsystem (ProgramUtilTest compiles that file without it).
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
|
||||
}
|
||||
MGLOG_D("glslang initialized");
|
||||
g_isInitialized = true;
|
||||
MGLOG_I("MobileGL initialized");
|
||||
@@ -224,11 +100,9 @@ namespace MobileGL {
|
||||
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
||||
// via EnsureInitialized(), and full teardown happens deterministically
|
||||
// when the last EGL display is terminated with nothing current (EGLImpl
|
||||
// calls Destroy()). There is intentionally no backend-initializing static
|
||||
// constructor, no static destructor, and no DllMain: the global singletons
|
||||
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||
// calls Destroy()). There is intentionally no static constructor, no
|
||||
// static destructor, and no DllMain: the global singletons use
|
||||
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||
// without eglTerminate simply leaks them to the OS instead of running
|
||||
// backend destructors during static teardown. macOS has a lightweight
|
||||
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
|
||||
// initialization still enters here from the first hooked CGL context.
|
||||
// backend destructors during static teardown.
|
||||
} // namespace MobileGL
|
||||
|
||||
+3
-4
@@ -13,10 +13,9 @@ namespace MobileGL {
|
||||
void Initialize();
|
||||
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
|
||||
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
|
||||
// full backend initialization never depends on ELF/DLL static constructors,
|
||||
// and so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate). The macOS dyld bootstrap installs only lightweight
|
||||
// NSOpenGL method hooks.
|
||||
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
|
||||
// so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate).
|
||||
void EnsureInitialized();
|
||||
void Destroy();
|
||||
|
||||
|
||||
@@ -9,15 +9,11 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
class FramebufferObject;
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
}
|
||||
|
||||
enum class BackendType {
|
||||
@@ -28,34 +24,6 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
namespace MG_Backend {
|
||||
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
|
||||
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
|
||||
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
|
||||
// one of the two pointers is set; neither is set when the name named nothing, which is
|
||||
// the INVALID_VALUE the frontend validator reports.
|
||||
struct CopyImageEndpoint {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
|
||||
Bool IsRenderbuffer() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Renderbuffer != nullptr;
|
||||
}
|
||||
Bool Exists() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Texture != nullptr || Renderbuffer != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
enum class FormatCapability : Uint64 {
|
||||
Creatable = 1ull << 0,
|
||||
|
||||
@@ -177,10 +145,6 @@ namespace MobileGL {
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
@@ -192,9 +156,9 @@ namespace MobileGL {
|
||||
GLsizei height, GLint border);
|
||||
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height);
|
||||
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
||||
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void (*GenerateMipmap)(GLenum target);
|
||||
@@ -210,35 +174,18 @@ namespace MobileGL {
|
||||
void (*MemoryBarrierByRegion)(GLbitfield barriers);
|
||||
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
|
||||
GLenum access, GLenum format);
|
||||
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
|
||||
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
|
||||
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
|
||||
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
|
||||
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
|
||||
// 64-bit and float/double widths are derived there from the same answer, which is why
|
||||
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
|
||||
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
|
||||
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
|
||||
// case for at all.
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
|
||||
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
|
||||
// size - all of which are frontend link artifacts on ProgramObject, and
|
||||
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
|
||||
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
|
||||
// one, or a SPIR-V module), in a namespace the application never sees.
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
|
||||
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
|
||||
// the application passes is the frontend interface-query enumeration's, and no
|
||||
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
|
||||
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
|
||||
// The name is the one coordinate all three agree on, so the frontend resolves the
|
||||
// index against its own enumeration and each backend maps the name to its own.
|
||||
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
|
||||
GLuint storageBlockBinding);
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
||||
GLsizei* length, GLchar* name);
|
||||
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
// GL fence sync objects. All entries are optional (may be null); the
|
||||
// frontend then falls back to always-signaled sync semantics.
|
||||
// FenceSync may itself return null when the backend cannot create a
|
||||
@@ -273,39 +220,6 @@ namespace MobileGL {
|
||||
// and leave the query readable later.
|
||||
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
||||
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
|
||||
// the frontend then rejects the target). Results/deletion flow through
|
||||
// GetQueryResult64 / DeleteBackendQuery like timer queries.
|
||||
BackendQueryHandle (*BeginOcclusionQuery)();
|
||||
void (*EndOcclusionQuery)(BackendQueryHandle query);
|
||||
// Transform feedback primitive queries backed by real GPU query pools
|
||||
// (optional; null = frontend falls back to CPU accounting).
|
||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
|
||||
// frontend's own accounting wherever that accounting is exact - a capture with no
|
||||
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
|
||||
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
|
||||
// the written count for a vertex-only capture that follows a large render pass,
|
||||
// where the desktop-exact answer is the one the frontend already computed. Defaults
|
||||
// to false, so a backend that never sets it keeps using its GPU result.
|
||||
Bool PrefersCpuXfbPrimitiveAccounting = false;
|
||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||
// called while the frontend capture state is still active, so the backend
|
||||
// can still see the capture program and buffer bindings.
|
||||
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
|
||||
void (*PatchParameteri)(GLenum pname, GLint value);
|
||||
void (*BeginTransformFeedback)(GLenum primitiveMode);
|
||||
void (*EndTransformFeedback)();
|
||||
// ARB_transform_feedback2. A backend that leaves these null keeps the single
|
||||
// implicit capture span the frontend has always modelled; the frontend state
|
||||
// (paused flag, per-object bindings) is tracked either way.
|
||||
void (*PauseTransformFeedback)();
|
||||
void (*ResumeTransformFeedback)();
|
||||
void (*BindTransformFeedback)(GLuint name);
|
||||
void (*DeleteTransformFeedback)(GLuint name);
|
||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||
};
|
||||
struct GlobalBackendFunctionsTable {
|
||||
@@ -332,13 +246,7 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
Uint64 UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
Uint64 ShaderStorageBufferOffsetAlignment = 256;
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
@@ -364,13 +272,6 @@ namespace MobileGL {
|
||||
Int MaxIntegerSamples = 1;
|
||||
Int MaxSamples = 1;
|
||||
Int MaxSampleMaskWords = 1;
|
||||
// Tessellation limits; defaults are the GL 4.0 core minimums.
|
||||
Int MaxPatchVertices = 32;
|
||||
Int MaxTessGenLevel = 64;
|
||||
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
|
||||
// minimums, which every ES 3.1 driver also guarantees.
|
||||
Int MinProgramTextureGatherOffset = -8;
|
||||
Int MaxProgramTextureGatherOffset = 7;
|
||||
Int MaxTextureImageUnits = 32;
|
||||
Int MaxVertexTextureImageUnits = 32;
|
||||
Int MaxComputeTextureImageUnits = 32;
|
||||
@@ -378,41 +279,10 @@ namespace MobileGL {
|
||||
Int MaxVertexAttribs = 16;
|
||||
Int MaxComputeShaderStorageBlocks = 8;
|
||||
Int MaxCombinedShaderStorageBlocks = 32;
|
||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
|
||||
// non-compute, non-fragment stages and these defaults are the spec minimums, not
|
||||
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
|
||||
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
|
||||
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
|
||||
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
|
||||
// backend that cannot honour a graphics-stage storage block MUST report 0 here
|
||||
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
|
||||
// does not make the block work; it only moves the failure from an honest
|
||||
// "unsupported" at query time to a backend link error the frontend never surfaces,
|
||||
// after which every draw with that program silently renders nothing.
|
||||
Int MaxVertexShaderStorageBlocks = 0;
|
||||
Int MaxTessControlShaderStorageBlocks = 0;
|
||||
Int MaxTessEvaluationShaderStorageBlocks = 0;
|
||||
Int MaxGeometryShaderStorageBlocks = 0;
|
||||
Int MaxFragmentShaderStorageBlocks = 8;
|
||||
Int MaxComputeUniformBlocks = 12;
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
|
||||
// axis. These six, with the invocations limit above, are the only indexed limits a
|
||||
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
|
||||
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
|
||||
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
|
||||
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
|
||||
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
|
||||
// answers, like the invocations limit: GL_Getter and the compile environment floor
|
||||
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
|
||||
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
|
||||
// MaxClipDistances' does.
|
||||
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
|
||||
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
Int TextureBufferOffsetAlignment = 1;
|
||||
Int MaxUniformBufferBindings = 24;
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
@@ -423,142 +293,15 @@ namespace MobileGL {
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
||||
// backend that cannot host a clip distance MUST report it: advertising eight the
|
||||
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
||||
// from an honest "unsupported" at query time to a backend shader-compile error the
|
||||
// frontend never surfaces, after which every draw with that program silently renders
|
||||
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
||||
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
||||
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
// backend that cannot host a cull distance MUST report it. The failure this prevents
|
||||
// is worse than the clip one, because cull distance discards the whole primitive:
|
||||
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
|
||||
// gl_MaxCullDistances from it, SPIRV-Cross then emits
|
||||
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
|
||||
// without that extension rejects the program in an info log nobody surfaces. These
|
||||
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
|
||||
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
|
||||
// is: they describe the no-backend case (standalone compiles, unit tests).
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
|
||||
// a convention is a statement about behaviour, so a backend that does not pin one
|
||||
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
|
||||
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
|
||||
// capability is absent: without the viewport array extension only viewport 0 is ever
|
||||
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
|
||||
// both - which vertex provokes is decided per pipeline by
|
||||
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
||||
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
||||
// of the backend.
|
||||
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Int MaxViewportWidth = 16384;
|
||||
Int MaxViewportHeight = 16384;
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
// GL 4.x fragment-interpolation offset limits. These defaults are the
|
||||
// core minimums and are replaced by live GLES/Vulkan device limits.
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
// For four fractional bits the greatest required legal offset is
|
||||
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// Whether a framebuffer whose depth and stencil attachments are distinct
|
||||
// images can be rendered to. GL only requires support when both refer to the
|
||||
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
|
||||
// otherwise, which is what DirectVulkan (one combined attachment) and the
|
||||
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
|
||||
// that never sets it keeps the permissive behaviour.
|
||||
Bool SupportsDistinctDepthStencilAttachments = true;
|
||||
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
|
||||
// renders to that layer. DirectGLES hands the layer straight to
|
||||
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
|
||||
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
|
||||
// so a backend that never sets it gets the conservative answer.
|
||||
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
|
||||
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
|
||||
// set for each supported target. Deliberately per target rather than one flag: the three
|
||||
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
|
||||
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
|
||||
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
|
||||
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
|
||||
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
|
||||
// a backend that never sets it gets the conservative answer.
|
||||
Uint32 PerLayerFramebufferAttachmentTargets = 0;
|
||||
|
||||
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
|
||||
return (static_cast<Int>(target) >= 0 &&
|
||||
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
|
||||
? (1u << static_cast<Uint32>(target))
|
||||
: 0u;
|
||||
}
|
||||
|
||||
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
|
||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||
}
|
||||
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
|
||||
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
|
||||
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
|
||||
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
|
||||
// both report VK_FALSE, so no real mobile device does.
|
||||
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
|
||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
|
||||
// profile") and the demotion there is mathematically mandatory, always.
|
||||
// Defaults to false so a backend that never sets it - and the no-backend case, which
|
||||
// is what standalone shader compiles and the unit tests run under - keeps the
|
||||
// demotion, which is the behaviour that works everywhere.
|
||||
Bool SupportsShaderFloat64 = false;
|
||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||
//
|
||||
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
|
||||
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
|
||||
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
|
||||
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
|
||||
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
|
||||
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||
// independently from the EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||
// varying program-wide (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||
// overrides the detection in either direction at backend init.
|
||||
//
|
||||
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||
// sets it gets standard modules and, at worst, the old honest declines.
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
Uint32 SubgroupSupportedFeatures = 0;
|
||||
|
||||
@@ -15,45 +15,4 @@
|
||||
namespace MobileGL::MG_Backend {
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
|
||||
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
|
||||
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
|
||||
//
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
|
||||
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
|
||||
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
|
||||
// to happen at its top. Two consequences are load-bearing rather than tidy:
|
||||
//
|
||||
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
|
||||
// a counter while the final line is produced;
|
||||
// * the apply thread is joined before pActiveBackendObject.reset(), so the server's own
|
||||
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
|
||||
// owns the context, by ServerLoop::Stop, rather than on the app thread.
|
||||
//
|
||||
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
|
||||
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
|
||||
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
|
||||
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
|
||||
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
|
||||
void ShutdownSplitRoles();
|
||||
|
||||
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
|
||||
//
|
||||
// Returns true when this process took the split path - whether the bring-up succeeded or
|
||||
// was refused by name - and the caller must then NOT call Init() a second time. Returns
|
||||
// false under MG_Config::TransportMode::Monolith, where there is nothing to bring up and
|
||||
// Init() keeps its own place after MG_State::Init().
|
||||
//
|
||||
// WHY IT EXISTS AT ALL, rather than MobileGL::Initialize simply calling Init() earlier:
|
||||
// the monolith arm of Init() must keep running after MG_State::Init(), because
|
||||
// BackendObject_DirectGLES::Initialize() is the tree's own order for it and moving it is a
|
||||
// pull-build change G1 forbids. So the split arm - and only the split arm - moves.
|
||||
//
|
||||
// It exists ONLY in a split build, for the reason InitServerRoleForSpawn states: a
|
||||
// definition guarded only on the inside still emits a symbol, and G1 measured that.
|
||||
Bool InitSplitRolesBeforeState();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,16 +18,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const MG_External::GLESCapabilities& capabilities,
|
||||
FormatCapabilityCache& cache);
|
||||
|
||||
// Clamps a requested sample count down to what the ES driver can really deliver for this
|
||||
// format on this format-capability target: the probed per-format list when there is one, the
|
||||
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
|
||||
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
|
||||
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
|
||||
// allocation call has to come through here. The shadow state keeps the requested count, so
|
||||
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
|
||||
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
||||
Int samples);
|
||||
|
||||
class BackendObject_DirectGLES : public BackendObject {
|
||||
public:
|
||||
~BackendObject_DirectGLES() override;
|
||||
@@ -77,13 +67,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
|
||||
#pragma once
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Uint g_backendContextGeneration;
|
||||
|
||||
struct ContextEpoch {
|
||||
Uint Native = 0;
|
||||
Uint64 Served = 0;
|
||||
Bool Server = false;
|
||||
Bool operator==(const ContextEpoch&) const = default;
|
||||
};
|
||||
|
||||
inline ContextEpoch CurrentContextEpoch() {
|
||||
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,16 +23,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
|
||||
// Native/server-only color level readback. Returns tightly packed, owned
|
||||
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
|
||||
// The caller resolves identity/extent from its resource record and supplies
|
||||
// whether the SOURCE allocation already uses an image carrier.
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
|
||||
#endif
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -50,8 +40,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride);
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex);
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
|
||||
@@ -71,10 +59,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
@@ -86,9 +70,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -102,7 +86,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
||||
GLchar* name);
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
Bool InitWindowSurface(NativeWindowType window);
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
Bool MakeCurrent();
|
||||
@@ -127,24 +121,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// capability read needs no current ES context, and it stays false until
|
||||
// the ES capabilities have been filled in.
|
||||
Bool AreTimerQueriesSupported();
|
||||
// True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or
|
||||
// EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as
|
||||
// core since 3.1, so the frontend advertises them unconditionally and an app may call
|
||||
// glTexBuffer whenever it likes; this is the only thing standing between that call and a
|
||||
// null entry point. False also means every shader declaring a samplerBuffer is
|
||||
// uncompilable on this driver, which the program build reports by name.
|
||||
Bool AreBufferTexturesSupported();
|
||||
// Human-readable name of the buffer-texture tier for diagnostics and the driver POST:
|
||||
// "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported".
|
||||
const char* GetBufferTextureTierName();
|
||||
// glTexBuffer / glTexBufferRange through whichever spelling this driver's buffer-texture
|
||||
// support actually ships: the unsuffixed names are ES 3.2 core, while an EXT/OES driver
|
||||
// exports glTexBuffer{,Range}EXT / OES. Callers must have checked
|
||||
// AreBufferTexturesSupported() first. CallTexBufferRange reports whether it could honour
|
||||
// the range - no tier is required to expose the range form, and the whole-buffer form is
|
||||
// the documented fallback.
|
||||
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer);
|
||||
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
|
||||
// creators return null (the frontend then falls back to an immediately
|
||||
// available zero result) when the calling thread does not own the ES
|
||||
@@ -154,16 +130,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
|
||||
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
|
||||
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
|
||||
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
|
||||
// which case the frontend falls back to counting primitives from the draw calls.
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||
// null or stale-generation handles IS final: the frontend may cache it
|
||||
@@ -180,69 +146,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
||||
Uint64 CurrentFrameSerial();
|
||||
Uint64 CompletedFrameSerial();
|
||||
// Block (up to timeoutNs) until the given frame serial provably retired on the
|
||||
// GPU, using the per-frame fence ring. False when no usable fence covers the
|
||||
// serial (fence-less context, foreign thread, or the slot was recycled);
|
||||
// completion state is untouched in that case.
|
||||
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
|
||||
// Applies (or defers until the window surface exists) the app-requested
|
||||
// eglSwapInterval on the native EGL surface.
|
||||
void SetSwapInterval(Int interval);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 review fix: the swap interval an ended server session asked for is not the next one's.
|
||||
// The request outlives the context on purpose (a surface re-created mid-session re-applies it),
|
||||
// so the in-process display server's session end forgets it: the next session starts at the
|
||||
// driver's default until it asks (BackendObject_DirectGLES's destructor under a transport).
|
||||
void ForgetRequestedSwapInterval();
|
||||
#endif
|
||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
void DestroyEGLContext();
|
||||
|
||||
// Transform feedback capture spans, performed by the real ES driver. The
|
||||
// capture set is declared on the backend program at link time; the driver-side
|
||||
// begin is deferred to the first draw of the span (ES needs the capturing
|
||||
// program current and the capture buffers bound), and the end also mirrors the
|
||||
// captured bytes back into the frontend buffer shadows.
|
||||
void PatchParameteri(GLenum pname, GLint value);
|
||||
|
||||
namespace XfbImpl {
|
||||
Bool AreTransformFeedbacksSupported();
|
||||
// True while a capture span is open on the current transform feedback object
|
||||
// (frontend Begin seen and not paused), whether or not the deferred driver-side
|
||||
// Begin has been issued yet. Draw paths that would restructure the primitive
|
||||
// stream, or that need to dispatch compute mid-draw, decline while it is set.
|
||||
Bool IsCaptureSpanOpen();
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback();
|
||||
void PauseTransformFeedback();
|
||||
void ResumeTransformFeedback();
|
||||
void BindTransformFeedback(GLuint name);
|
||||
void DeleteTransformFeedback(GLuint name);
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace XfbImpl
|
||||
|
||||
namespace RenderStateImpl {
|
||||
// Pushes the frontend's render-state block to the ES driver, diffed against what was
|
||||
// last pushed.
|
||||
//
|
||||
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
|
||||
// mask handed to the driver. A draw into a colour attachment the backend widened from
|
||||
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
|
||||
// move the stored alpha away from the 1.0 the application's three-channel format
|
||||
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
|
||||
// gets there in the first place, so it must be allowed to write alpha - hence the flag
|
||||
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
|
||||
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
|
||||
// frontend version.
|
||||
//
|
||||
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
|
||||
// answers from the frontend state, which this function only reads.
|
||||
void SyncRenderState(Bool forColorClear = false);
|
||||
void InvalidateSyncedRenderState();
|
||||
} // namespace RenderStateImpl
|
||||
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
+658
-12478
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <Config.h>
|
||||
#include "DirectGLES.h"
|
||||
|
||||
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
|
||||
// points on OpenGL ES, which has neither in core.
|
||||
//
|
||||
// Every strategy below is an emulation; they differ only in which driver capability
|
||||
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
|
||||
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
|
||||
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
|
||||
//
|
||||
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
|
||||
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
|
||||
// Indirect N x glDrawElementsIndirect N + 1 upload
|
||||
// BaseVertex N x glDrawElementsBaseVertex N
|
||||
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
|
||||
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
|
||||
// rebased index stream
|
||||
//
|
||||
// Which one runs is resolved once per ES context from the driver's capabilities,
|
||||
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
|
||||
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
|
||||
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// The tier this ES context resolved to, computed on first use and stable after.
|
||||
MG_Config::GLESMultiDrawMode ResolvedTier();
|
||||
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
|
||||
const char* TierName(MG_Config::GLESMultiDrawMode tier);
|
||||
// One line naming the resolved tier, the tiers the driver can support, and the env
|
||||
// clamp if one applied. For DriverPost and the startup log.
|
||||
String DescribeTierResolution();
|
||||
|
||||
// The resolution itself, as a pure function of a capability set: the backend feeds
|
||||
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
|
||||
// standalone, and both therefore report the same tier. `explanation`, when non-null,
|
||||
// receives the "requested -> resolved (driver supports: ...)" line.
|
||||
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& funcs,
|
||||
MG_Config::GLESMultiDrawMode requested, String* explanation);
|
||||
// Whether one tier is runnable on the given capability set, for per-row POST output.
|
||||
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||
MG_Config::GLESMultiDrawMode tier);
|
||||
|
||||
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
|
||||
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
|
||||
// base vertex is 0). Owns the whole draw, preparation included: callers must not
|
||||
// have run PrepareForDraw, because the compute tier has to dispatch before the
|
||||
// draw state is established.
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
|
||||
// The ES context is gone: every scratch buffer and the compute program belonged to
|
||||
// it, so drop the names without deleting them (the dead context reclaims them).
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||
@@ -1,785 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/SlotTables.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#endif
|
||||
|
||||
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
|
||||
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
|
||||
//
|
||||
// What changes, and why each of them is the point:
|
||||
//
|
||||
// * The KEY stops being a frontend heap address. It is MGPipeHandle{Slot, Gen}, minted by the
|
||||
// client's MGPipeSlotAllocator off the frontend object's GetLifetimeId(). A recycled heap
|
||||
// address cannot reproduce a handle, so the weak_ptr the registry carried per entry purely
|
||||
// to catch that (its Entry::stateRef, used as an IDENTITY test) stops being an identity
|
||||
// mechanism, and OwnerEquals / TwinLookupMemo x3 / UnitSamplerLookupMemo's owner compare all
|
||||
// lose their reason to exist.
|
||||
// * The lookup stops being a hash probe into an open-addressed map and becomes one bounds
|
||||
// check plus one array index, so a returned BackendPtr* is NOT invalidated by the next Find
|
||||
// on the table. That kills the hazard Managers.h documents at length, and with it the
|
||||
// by-value copy plus second Find that SyncTextureObjectToBackend paid to survive it.
|
||||
// * Slots are dense per kind, which is what lets the server side (ARCHITECTURE.md 10.1,
|
||||
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
|
||||
//
|
||||
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
|
||||
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
|
||||
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
|
||||
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
|
||||
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
|
||||
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
|
||||
// goes. So:
|
||||
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
|
||||
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
|
||||
// * a twin, and the driver storage it owns, is freed when the application lets go of the
|
||||
// object rather than up to 64 creations or 1024 draw ticks later. That is what
|
||||
// Managers.h's "dead gigabytes" note asked for.
|
||||
//
|
||||
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
|
||||
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
|
||||
// length of a test, and a context reset does the same in reverse - and the slot allocator
|
||||
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
|
||||
// resolved again by the next would find nothing to resolve. Every table therefore links itself
|
||||
// into a per-table-type list at construction and out at destruction, and one notice resolves
|
||||
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
|
||||
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
|
||||
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
|
||||
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
|
||||
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
|
||||
//
|
||||
// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the
|
||||
// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy
|
||||
// death notice walks it. It is never an identity test - that is what Gen is for - and it is
|
||||
// never read to decide whether an entry is dead: a destructor that runs after exit() has begun
|
||||
// has its notice dropped by InProcessTeardown(), and that twin is then a DELIBERATE leak (the
|
||||
// process is exiting, the driver reclaims the object, and a twin destructor must not call into
|
||||
// a driver that may already be unloaded), not something to be collected later.
|
||||
//
|
||||
// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers
|
||||
// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out
|
||||
// of server memory was the last place the server could hold one across records - and a caller
|
||||
// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside
|
||||
// the scope that names the debt, so the read is one greppable site rather than a property of
|
||||
// the iteration.
|
||||
//
|
||||
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
|
||||
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
|
||||
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
|
||||
// under a real split neither the frontend object nor its lifetime id exists on this side of
|
||||
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
|
||||
// belong on the client, and the backend should receive the handle in the verb payload. It is
|
||||
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
|
||||
// so nothing catches it automatically.
|
||||
//
|
||||
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
|
||||
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
|
||||
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
|
||||
// allocator's own three entries carry, repeated here so the refusal names this surface), and
|
||||
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
|
||||
//
|
||||
// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf,
|
||||
// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport
|
||||
// apply before touching a frontend object, with or without a barrier or named scope.
|
||||
// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef.
|
||||
// The registry guard is independent of Magma's legacy allocator debt scope.
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
// Declared in Managers.h as well; repeated here because this header is included from it
|
||||
// before that declaration, and the table below is the arming site on this arm (D13: "the
|
||||
// arming site moves to the slot table's first insertion").
|
||||
void EnsureProcessTeardownSentinel();
|
||||
|
||||
// What the two knobs add up to. Split out as a PURE function of them so a test can drive
|
||||
// every combination without needing a process per combination.
|
||||
enum class EsprytSlotArmVerdict {
|
||||
Handles, // kMGPipeSubsystemEsprytSlots is set: the {slot, gen} tables run.
|
||||
Legacy, // the bit is clear and the legacy address-keyed registry is reachable.
|
||||
NoArm, // the bit is clear AND MOBILEGL_PIPE_LEGACY_MEMOS=0 made the legacy arm
|
||||
// unreachable, so the operator asked for a configuration with no arm at all.
|
||||
};
|
||||
|
||||
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled);
|
||||
|
||||
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||
|
||||
// Says, at backend bring-up, that the knobs leave no arm - and does NOT stop.
|
||||
//
|
||||
// The stop cannot live here, and that is the whole point of the split. Backend context
|
||||
// creation runs inside eglMakeCurrent, and the integration harness pre-flights exactly that
|
||||
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
|
||||
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
|
||||
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
|
||||
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
|
||||
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
|
||||
// which happens in the test body where the harness reports it as a failure.
|
||||
//
|
||||
// The CALL SITE (InitDisplayAndContext in DirectGLES.cpp) is pinned by
|
||||
// DirectGLESSlotTable.EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs
|
||||
// the real bring-up entry point under the pair in a forked child: edit that site back to
|
||||
// ResolveEsprytSlotTablesArm() and the case fails naming both knobs.
|
||||
void DiagnoseEsprytSlotArm();
|
||||
|
||||
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
|
||||
// left no arm at all. Cold: called exactly once per process, from the latch below - i.e. at
|
||||
// the first twin lookup, which is the first moment an arm is actually needed. A process
|
||||
// that never twins anything needs no arm and is not stopped.
|
||||
Bool ResolveEsprytSlotTablesArm();
|
||||
|
||||
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
|
||||
// two arms hold their twins in different containers, so flipping mid-run would strand them.
|
||||
//
|
||||
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
|
||||
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
|
||||
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
|
||||
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
|
||||
// branch, and the arm dispatch folds into the caller.
|
||||
inline Bool EsprytSlotTablesEnabled() {
|
||||
static const Bool enabled = ResolveEsprytSlotTablesArm();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable {
|
||||
public:
|
||||
using StatePtr = SharedPtr<StateObject>;
|
||||
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||
using BackendPtr = SharedPtr<BackendObject>;
|
||||
|
||||
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
|
||||
// payload. Slots are dense and allocated per kind, so a million of one kind is already
|
||||
// far past any application's live object count; the cap is here because the alternative
|
||||
// is letting a corrupt 32-bit slot decide a vector resize. See GetOrCreate(MGPipeHandle).
|
||||
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
|
||||
|
||||
// P5e (id), CONTRACT-P5E §4.3: THE COMPOSITE BAND, and it is a P5e PREREQUISITE rather
|
||||
// than a follow-up. ShaderCso's top 1/16 of slot space (from
|
||||
// kMGPipeShaderCsoCompositeSlotBase = 983040) is the program-pipeline composites'
|
||||
// (MGPipeHandles.h); EntryAt below indexes m_slots BY SLOT and resizes to it, so a
|
||||
// single composite used to grow g_backendProgramObjects to ~983k entries of ~40 B -
|
||||
// ~40 MB for one program pipeline. Today that is reachable only through
|
||||
// GetOrCreate(StatePtr) for a composite program; after the rekey the by-handle
|
||||
// resolution IS the ordinary path, so every composite bind would pay it.
|
||||
//
|
||||
// A SECOND VECTOR RATHER THAN MORE OF THE FIRST, exactly as the allocator
|
||||
// (SlotAllocator.h's KindState::BandSlots) and the applier (PipeApply.h's
|
||||
// CompositeShaderCsos) already do, and for the same reason: both spaces stay dense
|
||||
// against their own high-water mark, which is the property that lets the server index
|
||||
// rather than hash. The band is EMPTY for every kind but ShaderCso and costs those
|
||||
// kinds one empty Vector per table.
|
||||
static constexpr Bool kHasCompositeBand = (kKind == MG_Pipe::MGPipeKind::ShaderCso);
|
||||
static constexpr SizeT kMaxBandSlots =
|
||||
MG_Pipe::kMGPipeShaderCsoSlotLimit - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
|
||||
|
||||
struct Entry {
|
||||
BackendPtr backend;
|
||||
// THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the
|
||||
// minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and
|
||||
// the legacy death notice. Never compared against another object to decide identity
|
||||
// - that is what Gen is for - never dereferenced for its address, and never read to
|
||||
// decide whether the slot is dead: death is announced, not discovered. ForEachLive
|
||||
// stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server
|
||||
// memory on every step was the last place the server could hold one across records.
|
||||
StateWeakPtr stateRef;
|
||||
// The generation this entry's twin was built for. An entry whose Gen no longer
|
||||
// matches the allocator's is a twin of the slot's PREVIOUS owner.
|
||||
Uint32 Gen = 0;
|
||||
Bool Live = false;
|
||||
};
|
||||
|
||||
// Every constructor links the table into the per-type holder list and the destructor
|
||||
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
|
||||
// registry) is a holder for exactly as long as it exists. Copy and move carry the
|
||||
// ENTRIES and the memo; the links are the table's own and are never copied.
|
||||
BackendSlotTable() { LinkHolder(); }
|
||||
BackendSlotTable(const BackendSlotTable& other):
|
||||
m_slots(other.m_slots),
|
||||
m_band(other.m_band),
|
||||
m_nullTwin(other.m_nullTwin),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||
m_slots(std::move(other.m_slots)),
|
||||
m_band(std::move(other.m_band)),
|
||||
m_nullTwin(std::move(other.m_nullTwin)),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable& operator=(const BackendSlotTable& other) {
|
||||
if (this != &other) {
|
||||
m_slots = other.m_slots;
|
||||
m_band = other.m_band;
|
||||
m_nullTwin = other.m_nullTwin;
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
BackendSlotTable& operator=(BackendSlotTable&& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_slots = std::move(other.m_slots);
|
||||
m_band = std::move(other.m_band);
|
||||
m_nullTwin = std::move(other.m_nullTwin);
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~BackendSlotTable() { UnlinkHolder(); }
|
||||
|
||||
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
|
||||
// object's lifetime id, so two calls for the same live object always land on the same
|
||||
// slot, and a successor object at the same heap address never does.
|
||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
|
||||
// must not trap where the release build quietly does the documented thing.
|
||||
if (stateObj == nullptr) {
|
||||
// The registry this replaces inserted a null KEY and handed back that entry's
|
||||
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
|
||||
// exactly that tolerance), so a release build never dereferenced null here.
|
||||
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
|
||||
// entry, so a second null call was handed the same twin the first one got.
|
||||
// Resetting here instead would have destroyed it - an arm difference in the one
|
||||
// path that documents relying on this. One per-table parking slot, never live,
|
||||
// never handed a handle, because a null object has no identity and
|
||||
// therefore cannot have a {slot, gen}.
|
||||
return m_nullTwin;
|
||||
}
|
||||
|
||||
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
|
||||
// creation is the moment a driver-owned id starts needing a guarded destructor;
|
||||
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
|
||||
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// This overload mints from frontend identity and is monolith-only. Refuse
|
||||
// before either the client object or allocator is touched, even when a
|
||||
// legacy named scope is open and the current record is barriered.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)");
|
||||
#endif
|
||||
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"MGPipe slot space of kind %u is exhausted",
|
||||
static_cast<Uint32>(kKind));
|
||||
Entry& entry = EntryAt(handle.Slot);
|
||||
if (entry.Live && entry.Gen != handle.Gen) {
|
||||
// The slot was reclaimed and handed to a new object: the twin at it describes
|
||||
// driver ids the new state object never made.
|
||||
entry.backend.reset();
|
||||
}
|
||||
entry.Gen = handle.Gen;
|
||||
entry.Live = true;
|
||||
entry.stateRef = stateObj;
|
||||
// No creation tick and no sweep here. The registry this replaces needed both,
|
||||
// because nothing told it a texture or a renderbuffer had been DELETED and object
|
||||
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
|
||||
// now announces its own death from its destructor, so a dead twin's slot is already
|
||||
// back before the next creation asks for one.
|
||||
RememberHandle(stateObj->GetLifetimeId(), handle);
|
||||
return entry.backend;
|
||||
}
|
||||
|
||||
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
|
||||
// header records against itself at the top of the file.
|
||||
//
|
||||
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
|
||||
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
|
||||
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
|
||||
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
|
||||
// the call's payload, already minted by the side that owns minting; all this does is
|
||||
// index the slot, notice a generation that no longer matches (the slot was recycled,
|
||||
// so the twin at it describes driver ids the new resource never made) and hand back
|
||||
// the twin pointer. FindByHandle beside it is the same shape and already existed.
|
||||
//
|
||||
// No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes
|
||||
// it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold
|
||||
// by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys
|
||||
// on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle,
|
||||
// asked by the one caller that needs the object, that answers null for it. Which is the
|
||||
// same set the old stateRef-locking walk produced, decided at one site instead of in
|
||||
// the loop.
|
||||
//
|
||||
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
|
||||
// announcement is the family's own destroy call, not the shared death notice, and the
|
||||
// slot is freed by the CLIENT after that call returns.
|
||||
//
|
||||
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
|
||||
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
|
||||
// it its first caller.
|
||||
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"GetOrCreate(handle) named the reserved null handle");
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
|
||||
|
||||
// A slot index that ARRIVED in a payload indexes a vector this call would RESIZE,
|
||||
// and nothing between the payload and here bounds it: the applier's blob gates sit
|
||||
// in front of the vertex-input family, not in front of the resource family, which
|
||||
// dispatches ops->Create(record.Res, ...) straight through. There is no allocator
|
||||
// constant to check against on this side - the allocator is the client's - so this
|
||||
// is a sanity cap and is documented as one: kMaxHandleSlot entries of one kind is
|
||||
// already orders of magnitude past any real GL object count, while a corrupt 32-bit
|
||||
// slot asks for a four-billion-entry resize.
|
||||
if (handle.Slot >= kMaxHandleSlot) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// PH-2: handle.Slot is peer supplied on the disaggregated arm. Release builds
|
||||
// must publish a named protocol fault instead of silently returning m_nullTwin.
|
||||
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
|
||||
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
|
||||
"GetOrCreate(handle) named slot %u, past this table's %u bound",
|
||||
handle.Slot, kMaxHandleSlot);
|
||||
#else
|
||||
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
|
||||
handle.Slot, kMaxHandleSlot);
|
||||
return m_nullTwin;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Same arming as the minting overload, and for the same reason: twin creation is
|
||||
// the moment a driver-owned id starts needing a guarded destructor.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
|
||||
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
|
||||
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
|
||||
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
|
||||
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
|
||||
// a persistent map, a pooled store, released by a defaulted destructor that issues
|
||||
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
|
||||
// dead resource's generation, after which the incumbent's own FindByHandle refuses
|
||||
// it and it is silently handed a fresh, empty twin. That is a leak AND a resource
|
||||
// that loses its storage with no diagnostic, i.e. the shape commit d7655247 fixed
|
||||
// and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and
|
||||
// resets the twin, BACKWARD is refused - which is the same answer FindByHandle
|
||||
// below already gives the same input.
|
||||
Entry& entry = EntryAt(handle.Slot);
|
||||
if (entry.Live && entry.Gen > handle.Gen) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// PH-2: a stale peer generation must not silently shed the incumbent twin in a
|
||||
// release server. The funnel names the exact identity fault and notifies the peer.
|
||||
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
|
||||
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
|
||||
"GetOrCreate(handle) named generation %u at slot %u, behind live generation %u",
|
||||
handle.Gen, handle.Slot, entry.Gen);
|
||||
#else
|
||||
MOBILEGL_ASSERT(false,
|
||||
"GetOrCreate(handle) named generation %u at slot %u, which is BEHIND "
|
||||
"the live entry's %u - refusing rather than destroying the incumbent",
|
||||
handle.Gen, handle.Slot, entry.Gen);
|
||||
return m_nullTwin;
|
||||
#endif
|
||||
}
|
||||
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
|
||||
entry.Gen = handle.Gen;
|
||||
entry.Live = true;
|
||||
return entry.backend;
|
||||
}
|
||||
|
||||
// The generation of the LIVE entry at this slot, or 0 when the slot is out of range or
|
||||
// holds no live entry. It exists so a caller can DIAGNOSE - in a release build, where
|
||||
// MOBILEGL_ASSERT is inert - the refusal GetOrCreate(handle) above performs silently.
|
||||
Uint32 LiveGenAt(Uint32 slot) const {
|
||||
const Entry* const entry = EntryOrNull(slot);
|
||||
if (entry == nullptr) return 0;
|
||||
return entry->Live ? entry->Gen : 0;
|
||||
}
|
||||
|
||||
// Remember/read the frontend object last used by the monolith sync path.
|
||||
// Handles supplied to the server never need this weak state. Neither method
|
||||
// touches the allocator, so each carries its own unconditional apply guard.
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return;
|
||||
entry->stateRef = stateObj;
|
||||
}
|
||||
|
||||
// The frontend object noted for this handle, or null. The handle answers identity;
|
||||
// this answers only "which object was this twin last synced from".
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
const Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
|
||||
return entry->stateRef.lock();
|
||||
}
|
||||
|
||||
// P3a: the death half of the overload above, for a kind whose announcement is its own
|
||||
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
|
||||
// than destroying it in place, because the caller may still have to decide what
|
||||
// happens to the driver id it owns - Espryt pools it, deletes it, or parks it on the
|
||||
// deferred-release list when no context is current on this thread - and every one of
|
||||
// those outcomes has to be reached with the entry already retired, so a re-entrant
|
||||
// GetOrCreate from a twin destructor cannot resurrect it.
|
||||
//
|
||||
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
|
||||
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
|
||||
// Gen bump rides the next handout, so a double free cannot skip a generation). An
|
||||
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
|
||||
// left alone: the successor's own GetOrCreate resets it.
|
||||
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return BackendPtr{};
|
||||
BackendPtr dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
return dead;
|
||||
}
|
||||
|
||||
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
|
||||
// mutates the table, so the returned pointer survives any later Find on it; only a
|
||||
// GetOrCreate that grows the vector can move it, and callers that hold one across a
|
||||
// possible insertion still copy the BackendPtr out.
|
||||
BackendPtr* Find(StateObject* stateObj) {
|
||||
if (stateObj == nullptr) return nullptr;
|
||||
return FindByHandle(HandleOf(stateObj));
|
||||
}
|
||||
|
||||
const BackendPtr* Find(StateObject* stateObj) const {
|
||||
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
|
||||
}
|
||||
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
|
||||
return &entry->backend;
|
||||
}
|
||||
|
||||
// The handle this object's twin is keyed on, or the null handle. This is what a backend
|
||||
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
|
||||
//
|
||||
// A NULL answer is never memoised. The memo is per table and the allocator is per
|
||||
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
|
||||
// a cached "no handle" here would then outlive the twin's creation over there, and
|
||||
// nothing on this table's own acquire path would ever refresh it. A miss costs the
|
||||
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve frontend identity only in the monolith path. A cache hit is
|
||||
// subject to the same guard as a fresh allocator lookup.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf");
|
||||
#endif
|
||||
const Uint64 lifetimeId = stateObj->GetLifetimeId();
|
||||
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// P2 step e2's backend half. The frontend object with this lifetime id has just been
|
||||
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
|
||||
// return the slot to the allocator - in that order, because the allocator forgets the
|
||||
// lifetime id on Free and a holder told second could no longer resolve it.
|
||||
//
|
||||
// The slot is returned whether or not any holder still had a twin at it: the lifetime
|
||||
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
|
||||
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
|
||||
// entries without freeing) would otherwise stay allocated for the life of the process.
|
||||
//
|
||||
// STATIC, and deliberately so: a notice is about an object, not about a table, and
|
||||
// "which table holds it" is exactly the question that produced the two-holder leak.
|
||||
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
|
||||
// a second call for the same id answers false because the allocator no longer maps it.
|
||||
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
|
||||
// a frontend death is announced to the server by the object_death record (ct),
|
||||
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
|
||||
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
|
||||
#endif
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
|
||||
// destructor, which is a driver call, and nothing that outlives it may be a
|
||||
// reference into this holder.
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
holder->ReleaseTwinAt(handle);
|
||||
holder = next;
|
||||
}
|
||||
MG_Pipe::MGPipeSlots().Free(kKind, handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
|
||||
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
|
||||
// handle IS the resolution - it crossed in the record's payload - so the client's
|
||||
// allocator is never asked: under an active transport MGPipeSlots() is a client-only
|
||||
// surface (rule E, §3.1) and this function runs on the apply thread. Every holder
|
||||
// lets go exactly as the notice arm does, in the same successor-first order. What
|
||||
// does NOT happen here is the allocator Free: the slot's owner - the client -
|
||||
// returned it itself after the record went out (PipeFill.cpp's NotifyAndFree), and a
|
||||
// double Free would be refused by generation anyway. Idempotent against the kind's
|
||||
// own delete opcode, exactly as the notice arm is: a twin the delete already released
|
||||
// fails ReleaseTwinAt's generation check and the walk moves on.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
Bool released = false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
holder = next;
|
||||
}
|
||||
return released;
|
||||
}
|
||||
|
||||
// How many tables of this type exist right now. For the tests that pin the holder
|
||||
// list; nothing on a shipping path asks.
|
||||
static Uint32 HolderCount() {
|
||||
Uint32 count = 0;
|
||||
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr;
|
||||
holder = holder->m_nextHolder) {
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// P5e (id), CONTRACT-P5E §4.1: fn(MGPipeHandle, const BackendPtr& twin) over every
|
||||
// live, twinned entry. Replaces the registry's begin()/end(), whose iterator exposed
|
||||
// the raw frontend address as the map key - the one place the backend read an identity
|
||||
// it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a
|
||||
// frontend SharedPtr OUT OF SERVER MEMORY on every step.
|
||||
//
|
||||
// WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing.
|
||||
// The walk no longer locks Entry::stateRef, so a caller that needs the frontend object
|
||||
// asks StateForHandle(handle) for it - which is the same weak reference, read at one
|
||||
// named site inside the scope that owns the debt, instead of at every step of an
|
||||
// iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip
|
||||
// because its stateRef did not lock is one whose StateForHandle answers null, and the
|
||||
// caller skips it there instead.
|
||||
//
|
||||
// The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot
|
||||
// is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the
|
||||
// client minted, never the index into m_band.
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
// Index loop and a COPIED twin, not a range-for over references: fn is arbitrary
|
||||
// backend code, and a nested GetOrCreate on this table would resize m_slots and
|
||||
// invalidate both the iterator and any reference into the vector that outlives the
|
||||
// call. The one caller today happens not to insert; that is not a property the
|
||||
// walk should depend on.
|
||||
for (SizeT index = 0; index < m_slots.size(); ++index) {
|
||||
const Entry& entry = m_slots[index];
|
||||
if (!entry.Live || !entry.backend) continue;
|
||||
const BackendPtr twin = entry.backend;
|
||||
fn(MG_Pipe::MGPipeHandle{static_cast<Uint32>(index), entry.Gen}, twin);
|
||||
}
|
||||
for (SizeT index = 0; index < m_band.size(); ++index) {
|
||||
const Entry& entry = m_band[index];
|
||||
if (!entry.Live || !entry.backend) continue;
|
||||
const BackendPtr twin = entry.backend;
|
||||
fn(MG_Pipe::MGPipeHandle{
|
||||
static_cast<Uint32>(index) + MG_Pipe::kMGPipeShaderCsoCompositeSlotBase,
|
||||
entry.Gen},
|
||||
twin);
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 LiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_slots) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
for (const Entry& entry : m_band) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// The band's own live count, so a case can tell "the composite is twinned" from "the
|
||||
// ordinary table grew to reach it" - which is the whole assertion the band exists for
|
||||
// and is untestable from LiveCount alone.
|
||||
Uint32 CompositeLiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_band) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// How many entries each space has ALLOCATED, live or not. A leak case asserts on these
|
||||
// rather than on LiveCount: growth is what the band prevents, and a dense table that
|
||||
// never shrinks is invisible to a liveness count.
|
||||
SizeT OrdinaryCapacityForTest() const { return m_slots.size(); }
|
||||
SizeT CompositeCapacityForTest() const { return m_band.size(); }
|
||||
|
||||
private:
|
||||
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
|
||||
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
|
||||
// every holder has let go.
|
||||
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
|
||||
// Forget the memo whenever it names this slot, even if this table has no entry
|
||||
// there: a memo can be a handle learned from the allocator for an object another
|
||||
// holder twinned, and it must not survive the slot's next handout.
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
// The twin's destructor is a driver call and could, in principle, re-enter
|
||||
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
|
||||
// destructor may be a reference into m_slots: the twin is moved out into a local,
|
||||
// the entry is finished with, and only then is the local released.
|
||||
BackendPtr dead;
|
||||
{
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false;
|
||||
dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
}
|
||||
dead.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P5e (id): the two slot spaces, resolved in ONE place ---------------------------
|
||||
//
|
||||
// Every reader and writer below goes through these three, so a caller that forgot the
|
||||
// band cannot exist - the shape MGPipeSlotAllocator::EntryOf already uses one level
|
||||
// out. kHasCompositeBand folds to `false` at compile time for the five non-ShaderCso
|
||||
// instantiations, so their band is dead code and an empty Vector.
|
||||
static constexpr Bool SlotIsBanded(Uint32 slot) {
|
||||
return kHasCompositeBand && MG_Pipe::MGPipeIsCompositeShaderSlot(slot);
|
||||
}
|
||||
static constexpr SizeT IndexOfSlot(Uint32 slot) {
|
||||
return SlotIsBanded(slot)
|
||||
? static_cast<SizeT>(slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase)
|
||||
: static_cast<SizeT>(slot);
|
||||
}
|
||||
|
||||
// The entry a slot names, or null when its space has never grown that far. NEVER grows:
|
||||
// a lookup that resized would turn every miss into an allocation, which is the hazard
|
||||
// Find documents.
|
||||
Entry* EntryOrNull(Uint32 slot) {
|
||||
Vector<Entry>& table = SlotIsBanded(slot) ? m_band : m_slots;
|
||||
const SizeT index = IndexOfSlot(slot);
|
||||
if (index >= table.size()) return nullptr;
|
||||
return &table[index];
|
||||
}
|
||||
const Entry* EntryOrNull(Uint32 slot) const {
|
||||
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
|
||||
}
|
||||
|
||||
// Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting
|
||||
// overload because the allocator produced it, the handle overload against
|
||||
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
|
||||
// allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary
|
||||
// table never learns the band exists.
|
||||
Entry& EntryAt(Uint32 slot) {
|
||||
Vector<Entry>& table = SlotIsBanded(slot) ? m_band : m_slots;
|
||||
const SizeT index = IndexOfSlot(slot);
|
||||
if (index >= table.size()) table.resize(index + 1);
|
||||
return table[index];
|
||||
}
|
||||
|
||||
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
|
||||
m_memoLifetimeId = lifetimeId;
|
||||
m_memoHandle = handle;
|
||||
}
|
||||
void ForgetHandle() const {
|
||||
m_memoLifetimeId = 0;
|
||||
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
|
||||
// The holder list: intrusive and doubly linked, so registering and unregistering are
|
||||
// two pointer writes with no allocation, and its head is a constant-initialised
|
||||
// static - which is what lets the process-lifetime registry globals in Managers.cpp
|
||||
// link themselves in from their own constructors with no initialisation-order
|
||||
// question to answer. Single-threaded, like every table it links (the tables live and
|
||||
// die on the context thread, as the notice they answer does).
|
||||
void LinkHolder() {
|
||||
m_prevHolder = nullptr;
|
||||
m_nextHolder = s_firstHolder;
|
||||
if (s_firstHolder != nullptr) s_firstHolder->m_prevHolder = this;
|
||||
s_firstHolder = this;
|
||||
}
|
||||
void UnlinkHolder() {
|
||||
if (m_prevHolder != nullptr) {
|
||||
m_prevHolder->m_nextHolder = m_nextHolder;
|
||||
} else {
|
||||
s_firstHolder = m_nextHolder;
|
||||
}
|
||||
if (m_nextHolder != nullptr) m_nextHolder->m_prevHolder = m_prevHolder;
|
||||
m_prevHolder = nullptr;
|
||||
m_nextHolder = nullptr;
|
||||
}
|
||||
|
||||
static inline BackendSlotTable* s_firstHolder = nullptr;
|
||||
BackendSlotTable* m_prevHolder = nullptr;
|
||||
BackendSlotTable* m_nextHolder = nullptr;
|
||||
|
||||
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
|
||||
Vector<Entry> m_slots;
|
||||
// P5e (id): the ShaderCso COMPOSITE band, indexed by (slot -
|
||||
// kMGPipeShaderCsoCompositeSlotBase) and EMPTY for every other kind. See
|
||||
// kHasCompositeBand above for why it is a second vector and not more of the first.
|
||||
Vector<Entry> m_band;
|
||||
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
|
||||
BackendPtr m_nullTwin;
|
||||
|
||||
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
|
||||
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
|
||||
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
|
||||
// indexing".
|
||||
//
|
||||
// It is one entry and therefore only helps a caller that asks for the SAME object twice
|
||||
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
|
||||
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
|
||||
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
|
||||
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
|
||||
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
|
||||
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
|
||||
//
|
||||
// It cannot serve a stale answer, by three independent arguments:
|
||||
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
|
||||
// heap address cannot hit this memo the way it could hit an address-keyed one;
|
||||
// * a null answer is never stored, so another holder's acquire cannot be hidden by
|
||||
// a "no handle" this table remembered earlier; and
|
||||
// * even a hit for a slot that has since been freed and re-handed is caught, because
|
||||
// the caller resolves the handle through FindByHandle, which compares Gen.
|
||||
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
|
||||
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
|
||||
mutable Uint64 m_memoLifetimeId = 0;
|
||||
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,23 +9,8 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
|
||||
// THIS ROLE's backend. Under an active transport the server's own private
|
||||
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
|
||||
// lookup must read ITS probed cache - not pActiveBackendObject's, which under split is the
|
||||
// CLIENT's BackendObject_Remote mirror (a caps snapshot, generation-lagged, and on an
|
||||
// independent server not usable at all). Monolith build/transport: pActiveBackendObject, so a
|
||||
// pull build never sees this symbol. Null when no backend is up. The seven table-3 reads
|
||||
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
|
||||
// this instead of dereferencing pActiveBackendObject directly.
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps();
|
||||
#endif
|
||||
|
||||
namespace DebugImpl {
|
||||
class ErrorLopper {
|
||||
public:
|
||||
@@ -49,148 +34,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace VertexArrayImpl
|
||||
|
||||
namespace TextureImpl {
|
||||
// Whether images on this format-capability target can back a colour attachment, and so
|
||||
// need a colour-renderable storage format even when the frontend asked for a
|
||||
// three-channel one ES never renders to. Shared by the capability probe (which passes the
|
||||
// capabilities it has just queried, before the globals are published) and by the
|
||||
// allocation path (which reads the active backend's), so the format the cache was probed
|
||||
// with is always the format the image is created with.
|
||||
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||
|
||||
// Whether this format's ES storage is widened to 8-bit-per-channel because the
|
||||
// driver stores some packed16 allocations with a mirrored field order
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
|
||||
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
|
||||
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
|
||||
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
|
||||
// is no longer 16-bit packed.
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType);
|
||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
|
||||
// True when the format the image is actually created with has an alpha channel the
|
||||
// frontend format does not (the three-channel colour-renderable widening). GL reads such
|
||||
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
|
||||
// any readback of the image has to overwrite the alpha the draw happened to leave there.
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
|
||||
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
||||
// GLSL ES cannot spell as an image is carried by one it can.
|
||||
//
|
||||
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
||||
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
||||
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
||||
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
||||
// Seventeen have a core format of the SAME per-channel width and component type,
|
||||
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
||||
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so the carrier's surplus channels
|
||||
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
||||
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
||||
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
||||
//
|
||||
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
||||
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
||||
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
||||
// diagnostic rather than a silent approximation), and on a driver that HAS
|
||||
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
||||
// happen behind it.
|
||||
//
|
||||
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
||||
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
||||
// attachment behaviour, so the image constraint wins. In practice that only bites
|
||||
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
||||
// signed-normalized instead of becoming the half float that fallback would have picked -
|
||||
// so an image-bound texture in one of those two formats is no longer attachable, and
|
||||
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
||||
// does not update. Accepted deliberately: before the widening, an image binding in either
|
||||
// format was refused outright by every driver tested and the stage that declared it never
|
||||
// compiled at all, so nothing that works today is being given up.
|
||||
//
|
||||
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
||||
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
||||
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
||||
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
||||
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
||||
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
||||
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
||||
// from "alpha" to a channel count, which is its own change.
|
||||
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
|
||||
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
|
||||
// shadow already holds SourceChannels components of exactly the carrier's own type, so
|
||||
// padding it out to four is the whole conversion. The packed entries do not - their shadow
|
||||
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
|
||||
// type takes twelve or sixteen bytes out of four and shears the level.
|
||||
enum class ImageWidenSourceEncoding : Uint8 {
|
||||
Components = 0,
|
||||
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
|
||||
PackedFloat11f11f10f,
|
||||
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
|
||||
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
|
||||
// only in what the codes MEAN, which is the shader's business and not the transfer's.
|
||||
PackedInt2101010Rev,
|
||||
};
|
||||
|
||||
struct ImageBindableStorageWidening {
|
||||
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
||||
GLenum Format = GL_UNKNOWN_MGL;
|
||||
GLenum Type = GL_UNKNOWN_MGL;
|
||||
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
||||
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
||||
Uint SourceChannels = 0;
|
||||
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||
// RG8UI), so the carrier decides.
|
||||
Bool IntegerData = false;
|
||||
// What the upload has to do to the frontend shadow before it describes the level to
|
||||
// the driver (PrepareImageWidenedUpload).
|
||||
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
||||
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
||||
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
||||
// has no image format of any width for and which a float carrier would requantise.
|
||||
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
||||
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
||||
//
|
||||
// Two things depend on it, both because the ES storage no longer shares the frontend
|
||||
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
||||
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
||||
// one), and glGetTexImage divides the codes back out into the floats the application
|
||||
// is still owed.
|
||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||
Bool SignedNormalized = false;
|
||||
|
||||
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||
|
||||
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
|
||||
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
|
||||
//
|
||||
// A buffer texture cannot be widened: its texels are the application's buffer object, at
|
||||
// the size and layout the application gave it, and it is usually also a vertex, index or
|
||||
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
|
||||
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
|
||||
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
|
||||
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
|
||||
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
|
||||
// nothing.
|
||||
//
|
||||
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
|
||||
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
|
||||
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
|
||||
// reason the storage widening's gaps are - on a driver where the split applies at all
|
||||
// there is no legal ESSL for the image declaration, so such a program did not compile.
|
||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
@@ -246,16 +96,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams);
|
||||
|
||||
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
|
||||
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
|
||||
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
|
||||
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
|
||||
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
|
||||
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
|
||||
// False when `type` is not a 4-byte packed type.
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
@@ -264,289 +104,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
|
||||
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
|
||||
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
|
||||
// outputs and copies the value into them at the end of main. A no-op for
|
||||
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||
// SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
|
||||
// sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
|
||||
// On a driver that advertises only GL_OES_texture_buffer that directive is a compile
|
||||
// error, so the name is retargeted in the emitted source. A no-op on every other tier:
|
||||
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
||||
String RetargetTextureBufferExtension(String glslCode,
|
||||
MG_External::GLESCapabilities::TextureBufferTier tier);
|
||||
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
|
||||
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
|
||||
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
|
||||
// because only it knows which formats are in play AND whether the driver advertises the
|
||||
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||
// never emitted speculatively. A no-op when not needed or already present.
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
|
||||
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
|
||||
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
|
||||
// spelling for it at any version, so the request has to be made here or the stage does
|
||||
// not compile - which loses the whole program, not just the multi-viewport routing.
|
||||
// `needed` is the caller's answer for the same reason as above: only it knows whether the
|
||||
// driver advertises the extension, and requesting an unadvertised one is itself a compile
|
||||
// error, so this is never emitted speculatively. A no-op when not needed or already
|
||||
// present.
|
||||
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
||||
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
|
||||
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
|
||||
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
|
||||
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
|
||||
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
|
||||
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
|
||||
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
|
||||
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
|
||||
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
|
||||
// advertises neither spelling, because requesting an unadvertised extension is itself a
|
||||
// compile error. A no-op when nullptr or already present.
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName);
|
||||
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
|
||||
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
|
||||
// ones; the two extensions are separate and neither implies the other.
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
// format in, but SPIRV-Cross throws rather than printing the formats it calls
|
||||
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
|
||||
// stage. So those formats stay out of the module and are spelled here instead, on the
|
||||
// emitted text, where nothing can refuse them.
|
||||
//
|
||||
// Declarations that already carry a format are left exactly as they are, whoever wrote
|
||||
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
||||
// image array into; the suffix is the array's own name and the element's index.
|
||||
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
||||
// One image ARRAY whose elements the application pointed at units that are not
|
||||
// consecutive-from-element-zero.
|
||||
struct ImageArrayUnitPlan {
|
||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||
Vector<Int> units; // the frontend image unit element k has to reach
|
||||
};
|
||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||
// wrong value and three were never written, with no GL error and no link log. The same
|
||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
||||
// INVALID_OPERATION.
|
||||
//
|
||||
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
||||
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
||||
// element k. One declaration carries one binding, so one declaration per unit is the
|
||||
// only spelling that reaches an arbitrary set of them.
|
||||
//
|
||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||
// widening the array to cover the whole span of units and routing each subscript through
|
||||
// a `const highp int` offset table - was written before that pass covered images, and
|
||||
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
||||
// The split also costs exactly the image uniforms the application declared, where the
|
||||
// widening cost the whole SPAN (seven for the four elements of
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
||||
// fail to fit in.
|
||||
//
|
||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||
// caller to report - when the emitted extent disagrees with the reflection, when the
|
||||
// array is reached by anything other than a subscript, or when a subscript is not a
|
||||
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
||||
//
|
||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
||||
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
||||
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
||||
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined = nullptr);
|
||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||
// `out` one.
|
||||
//
|
||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||
// which members, depends on what the application's shader touched; a synthesized stage
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||
// bound in its place, and every draw silently renders nothing.
|
||||
//
|
||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
|
||||
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
|
||||
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
|
||||
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
|
||||
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
||||
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
||||
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
||||
// two that use it differently arrive at different ones - which is exactly the property
|
||||
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
||||
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
||||
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
||||
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
|
||||
// speculatively decorates every storage image NonWritable+NonReadable
|
||||
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
|
||||
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
|
||||
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
|
||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||
// up as a device compile failure - and then as a silently no-op draw.
|
||||
//
|
||||
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
||||
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
||||
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `coherent readonly
|
||||
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
||||
// every imageStore at the second one, and follow each of
|
||||
// those stores with `memoryBarrierImage();`. Several image
|
||||
// variables may share an image unit as long as they have
|
||||
// the same type and format, which is exactly what the pair
|
||||
// is.
|
||||
//
|
||||
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
||||
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
||||
// declared in two stages to be declared identically - so a shader that stores an image from
|
||||
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
||||
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
||||
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
||||
// still reads back its initial contents
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
||||
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
||||
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
||||
//
|
||||
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
||||
// point: two stages that use an image the same way emit byte-identical declarations, so
|
||||
// letting them keep one shared name costs nothing and merging them is correct, while two
|
||||
// stages that use it differently land on different prefixes and cannot be merged at all.
|
||||
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
||||
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
||||
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
||||
// binding_images_texture_type_* declares three read+write images in each of its five
|
||||
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
||||
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
|
||||
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
|
||||
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
|
||||
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
|
||||
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
|
||||
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
|
||||
// nothing but the identifier changed. Stages that agree still reach the same alias and
|
||||
// stay merged, so this costs no shader an extra image uniform.
|
||||
//
|
||||
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
|
||||
// readonly and writeonly (a spelling no access analysis produces, so it came from the
|
||||
// application and is identical everywhere), and one carrying NEITHER, which is legal only
|
||||
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
|
||||
//
|
||||
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||
// one when both are coherent, and the split is what makes a same-variable
|
||||
// read-after-write cross-variable. The single-declaration repairs above do not get it -
|
||||
// nothing aliases them.
|
||||
//
|
||||
// The barrier is the other half of the same problem, and coherent alone did not cover it:
|
||||
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
|
||||
// variable and a read of another it has no reason to believe alias, and is free to serve
|
||||
// the read from before the write - which is what advanced-memory-order's store/load/
|
||||
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
|
||||
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
|
||||
// non-uniform control flow. It costs something in a shader that stores to a read+write
|
||||
// image in a loop, which is why it is confined to the split pair.
|
||||
//
|
||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
|
||||
// uniforms, so a shader with more than half the limit in read+write images is the case
|
||||
// to watch.
|
||||
//
|
||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||
// already rewritten to and must run before those bindings are stripped - see the call
|
||||
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
||||
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
||||
//
|
||||
// `outSplitCount`, when given, receives the number of declarations that were actually
|
||||
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
||||
// application declared. Zero for every shader but a handful, and the only number the
|
||||
// budget note above can be reported with.
|
||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
|
||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
|
||||
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
|
||||
// shader as a uniform and be folded into every lookup's level of detail. Declares
|
||||
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
|
||||
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
|
||||
// the bound texture's (or sampler object's) value into it; a shader whose samplers
|
||||
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||
// there is nothing to rewrite.
|
||||
//
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
// Included in DirectGLES.cpp after the native packed-word readback helper.
|
||||
// The server needs owned tight rows for a storage promotion, not client PACK
|
||||
// layout or a fallback into a frontend texture's potentially stale shadow.
|
||||
class ScopedWireTextureReadLevel {
|
||||
public:
|
||||
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
|
||||
: m_texture(texture), m_target(target) {
|
||||
switch (target) {
|
||||
case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break;
|
||||
case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break;
|
||||
case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
|
||||
case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
|
||||
default: return;
|
||||
}
|
||||
GLint previous = 0;
|
||||
DrainESErrors();
|
||||
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(m_target, m_texture);
|
||||
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel);
|
||||
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel);
|
||||
m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
if (m_saved) {
|
||||
// ES framebuffer completeness can reject a defined level whose
|
||||
// base level or intervening mip chain is missing/inconsistent.
|
||||
// Read this existing native image as the sole base level. The
|
||||
// logical GL parameters and every cached copy remain unchanged.
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level);
|
||||
m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
|
||||
}
|
||||
|
||||
~ScopedWireTextureReadLevel() {
|
||||
if (!m_saved) return;
|
||||
// The packed-word path can use its own texture scratch binding.
|
||||
// Save the binding at restoration time too; do not assume an active
|
||||
// texture unit or make the backend's binding cache forget a bind.
|
||||
GLint previous = 0;
|
||||
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(m_target, m_texture);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel);
|
||||
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
|
||||
}
|
||||
|
||||
Bool Valid() const { return m_valid; }
|
||||
|
||||
private:
|
||||
GLuint m_texture;
|
||||
GLenum m_target;
|
||||
GLenum m_binding = GL_NONE;
|
||||
GLint m_baseLevel = 0, m_maxLevel = 0;
|
||||
Bool m_saved = false, m_valid = false;
|
||||
};
|
||||
|
||||
static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier,
|
||||
Vector<Uint8>& bytes) {
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) return false;
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (!pixelBytes || width <= 0 || height <= 0) return false;
|
||||
const GLenum componentType = QueryReadAttachmentComponentType();
|
||||
const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT;
|
||||
if (carrier.Active() ? (mapping.isInteger || !integerAttachment)
|
||||
: mapping.isInteger != integerAttachment) return false;
|
||||
|
||||
GLint implFormat = 0, implType = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
|
||||
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
|
||||
struct Candidate { GLenum format, type; };
|
||||
Vector<Candidate> candidates;
|
||||
if (carrier.Active()) {
|
||||
candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT});
|
||||
} else if (mapping.isInteger) {
|
||||
if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) &&
|
||||
(implType == GL_INT || implType == GL_UNSIGNED_INT))
|
||||
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
|
||||
candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)});
|
||||
} else {
|
||||
if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) &&
|
||||
(CanDecodeWideSourceType(implType) ||
|
||||
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV)))
|
||||
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
|
||||
if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT});
|
||||
if (componentType == GL_SIGNED_NORMALIZED) {
|
||||
candidates.push_back({GL_RGBA, GL_SHORT});
|
||||
candidates.push_back({GL_RGBA, GL_BYTE});
|
||||
} else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE});
|
||||
}
|
||||
ScopedPixelPackBuffer packBuffer(0);
|
||||
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||
const SizeT pixels = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
Int channels = 0;
|
||||
DrainESErrors();
|
||||
for (const auto candidate : candidates) {
|
||||
channels = GetWideReadChannelCount(candidate.format);
|
||||
wide.resize(pixels * static_cast<SizeT>(channels) * GetReadbackComponentSize(candidate.type));
|
||||
g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data());
|
||||
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; }
|
||||
}
|
||||
if (wideType == GL_NONE) return false;
|
||||
|
||||
if (carrier.Active()) {
|
||||
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(decoded.data());
|
||||
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
|
||||
for (SizeT i = 0; i < pixels; ++i) {
|
||||
for (SizeT c = 0; c < 4; ++c) {
|
||||
const Float divisor = static_cast<Float>(carrier.ChannelMax[c]);
|
||||
dst[i * 4 + c] = carrier.SignedNormalized
|
||||
? std::max(static_cast<Float>(static_cast<Int16>(static_cast<Uint16>(src[i * 4 + c]))) / divisor, -1.0f)
|
||||
: static_cast<Float>(src[i * 4 + c]) / divisor;
|
||||
}
|
||||
}
|
||||
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
|
||||
} else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
|
||||
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(decoded.data());
|
||||
for (SizeT i = 0; i < pixels; ++i) {
|
||||
Uint32 word = 0;
|
||||
Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word));
|
||||
dst[i * 4] = static_cast<Float>(word & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 3u) / 3.0f;
|
||||
}
|
||||
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
|
||||
}
|
||||
if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType);
|
||||
if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType);
|
||||
Vector<Uint8> converted(pixels * pixelBytes);
|
||||
const SizeT sourceRowBytes = static_cast<SizeT>(width) * 4 * GetReadbackComponentSize(wideType);
|
||||
for (GLsizei row = 0; row < height; ++row)
|
||||
ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast<SizeT>(row) * sourceRowBytes,
|
||||
converted.data() + static_cast<SizeT>(row) * width * pixelBytes, width, wideType, mapping, type);
|
||||
bytes = Move(converted);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target,
|
||||
TextureUploadTarget uploadTarget, GLint level,
|
||||
const IntVec3& extent, GLenum format, GLenum type,
|
||||
Vector<Uint8>& bytes) {
|
||||
const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel(
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (!pixelBytes) return false;
|
||||
const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL;
|
||||
const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
||||
ScopedFramebufferBinding bindings(true, false);
|
||||
GLuint fbo = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &fbo);
|
||||
struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo};
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo);
|
||||
ScopedPixelPackBuffer packBuffer(0);
|
||||
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT)
|
||||
: GL_STENCIL_ATTACHMENT;
|
||||
const SizeT count = static_cast<SizeT>(extent.x()) * extent.y();
|
||||
Vector<Uint8> result(count * extent.z() * pixelBytes);
|
||||
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
g_GLESFuncs.glFinish();
|
||||
for (GLint z = 0; z < extent.z(); ++z) {
|
||||
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
|
||||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
|
||||
g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z);
|
||||
else
|
||||
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point,
|
||||
TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level);
|
||||
g_GLESFuncs.glReadBuffer(GL_NONE);
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
|
||||
Vector<Float> depth;
|
||||
Vector<Uint8> stencil;
|
||||
if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false;
|
||||
if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false;
|
||||
auto* dst = result.data() + count * z * pixelBytes;
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
if (format == GL_DEPTH_STENCIL) {
|
||||
const Uint32 s = stencil[i];
|
||||
if (type == GL_UNSIGNED_INT_24_8) {
|
||||
const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s;
|
||||
Memcpy(dst + i * pixelBytes, &packed, sizeof(packed));
|
||||
} else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) {
|
||||
Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float));
|
||||
Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s));
|
||||
} else return false;
|
||||
} else if (type == GL_FLOAT) {
|
||||
const Float value = needDepth ? depth[i] : static_cast<Float>(stencil[i]);
|
||||
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
|
||||
} else if (type == GL_HALF_FLOAT) {
|
||||
const Uint16 value = MG_Util::EncodeFloatToHalfBits(
|
||||
needDepth ? depth[i] : static_cast<Float>(stencil[i]));
|
||||
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
|
||||
} else {
|
||||
const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT;
|
||||
const Double maximum = signedType
|
||||
? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0)
|
||||
: (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0);
|
||||
const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i];
|
||||
if (pixelBytes == 1) dst[i] = static_cast<Uint8>(value);
|
||||
else if (pixelBytes == 2) { const Uint16 v = static_cast<Uint16>(value); Memcpy(dst + i * 2, &v, 2); }
|
||||
else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4);
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes) {
|
||||
if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false;
|
||||
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level);
|
||||
if (!readLevel.Valid()) return false;
|
||||
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX)
|
||||
return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes);
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) return false;
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false;
|
||||
const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
const auto widening = sourceUsesImageCarrier
|
||||
? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{};
|
||||
const SizeT sliceBytes = static_cast<SizeT>(extent.x()) * extent.y() * pixelBytes;
|
||||
Vector<Uint8> result(sliceBytes * static_cast<SizeT>(extent.z()));
|
||||
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
g_GLESFuncs.glFinish();
|
||||
|
||||
// A raw packed word may have redundant encodings or NaN payloads. Read
|
||||
// it through R32UI when the source really still uses that native layout.
|
||||
const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (rawPacked && pixelBytes == sizeof(Uint32)) {
|
||||
const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
uploadTarget <= TextureUploadTarget::CubeMapNegativeZ
|
||||
? static_cast<GLint>(uploadTarget) - static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX) : 0;
|
||||
for (GLint layer = 0; layer < extent.z(); ++layer)
|
||||
if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(),
|
||||
reinterpret_cast<Uint32*>(result.data() + static_cast<SizeT>(layer) * sliceBytes))) return false;
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
NormalizedImageCarrierRead carrier{};
|
||||
if (widening && widening.CarriesNormalizedCodes()) {
|
||||
for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c];
|
||||
carrier.SignedNormalized = widening.SignedNormalized;
|
||||
}
|
||||
TempFBOBinder binder(true);
|
||||
auto& framebuffer = binder.Framebuffer();
|
||||
for (GLint layer = 0; layer < extent.z(); ++layer) {
|
||||
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
|
||||
ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer);
|
||||
else
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level);
|
||||
ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x",
|
||||
status, texture, level, nativeTarget);
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> slice;
|
||||
if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type,
|
||||
TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) ||
|
||||
slice.size() != sliceBytes) return false;
|
||||
Memcpy(result.data() + static_cast<SizeT>(layer) * sliceBytes, slice.data(), sliceBytes);
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
|
||||
const auto* record = PipeTextureRecordForHandle(info.Res);
|
||||
if (!record) return false;
|
||||
TextureTarget target = TextureTarget::Unknown;
|
||||
for (Int i = 0; i < static_cast<Int>(TextureTarget::TextureTargetCount); ++i)
|
||||
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)) == record->Desc.Target)
|
||||
target = static_cast<TextureTarget>(i);
|
||||
if (target == TextureTarget::Unknown) return false;
|
||||
const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res);
|
||||
if (!twin) return false;
|
||||
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
|
||||
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
|
||||
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
|
||||
Vector<Uint8> result;
|
||||
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
|
||||
const auto uploadTarget = wholeCube
|
||||
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
|
||||
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
|
||||
Vector<Uint8> slice;
|
||||
if (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
|
||||
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
|
||||
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
|
||||
result.insert(result.end(), slice.begin(), slice.end());
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
@@ -9,20 +9,15 @@
|
||||
#include "BackendObject_DirectVulkan.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "DirectVulkan.h"
|
||||
#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"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -45,11 +40,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool IsLayeredTarget(TextureTarget target) {
|
||||
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
|
||||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
|
||||
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
|
||||
target == TextureTarget::TextureCubeMapArray ||
|
||||
target == TextureTarget::Texture2DMultisampleArray;
|
||||
}
|
||||
|
||||
Bool IsMultisampleTarget(TextureTarget target) {
|
||||
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
|
||||
return target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray;
|
||||
}
|
||||
|
||||
Bool IsTextureBufferTarget(TextureTarget target) {
|
||||
@@ -61,8 +58,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum normalizedInternalFormat = glFormat;
|
||||
GLenum imageFormat = GL_RGBA;
|
||||
GLenum imageType = GL_UNSIGNED_BYTE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
|
||||
&normalizedInternalFormat, &imageFormat, &imageType);
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
||||
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
|
||||
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
|
||||
imageFormat == GL_RGBA_INTEGER;
|
||||
}
|
||||
@@ -83,7 +80,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return caps;
|
||||
}
|
||||
|
||||
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
|
||||
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat,
|
||||
TextureTarget target,
|
||||
VkFormatFeatureFlags features) {
|
||||
FormatCapabilityFlags caps;
|
||||
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
||||
@@ -102,7 +100,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
|
||||
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
|
||||
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
||||
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
||||
const Bool depthStencilRenderable =
|
||||
(features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
||||
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
|
||||
|
||||
if (sampled || renderable) {
|
||||
@@ -199,20 +198,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
|
||||
for (FormatCapability capability : kReportedFormatCapabilities) {
|
||||
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
|
||||
if (HasFormatCapability(fallbackCaps, capability) &&
|
||||
!HasFormatCapability(nativeCaps, capability)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
|
||||
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat,
|
||||
SizeT targetIndex,
|
||||
TextureInternalFormat fallbackFormat) {
|
||||
MGLOG_D(
|
||||
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
|
||||
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
|
||||
MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
|
||||
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
|
||||
}
|
||||
|
||||
Vector<Int> BuildSampleCounts(Int maxSamples) {
|
||||
@@ -256,15 +256,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
|
||||
? nativeProperties.bufferFeatures
|
||||
: nativeProperties.optimalTilingFeatures;
|
||||
const VkFormatFeatureFlags nativeFeatures =
|
||||
IsTextureBufferTarget(target) ? nativeProperties.bufferFeatures
|
||||
: nativeProperties.optimalTilingFeatures;
|
||||
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
|
||||
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
|
||||
|
||||
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
|
||||
? fallbackProperties.bufferFeatures
|
||||
: fallbackProperties.optimalTilingFeatures;
|
||||
const VkFormatFeatureFlags fallbackFeatures =
|
||||
IsTextureBufferTarget(target) ? fallbackProperties.bufferFeatures
|
||||
: fallbackProperties.optimalTilingFeatures;
|
||||
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
||||
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
|
||||
@@ -299,8 +299,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
|
||||
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
||||
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
|
||||
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
|
||||
FormatCapabilityFlags fallbackRenderbufferCaps =
|
||||
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D,
|
||||
fallbackProperties.optimalTilingFeatures);
|
||||
fallbackRenderbufferCaps &= FormatCapability::Creatable;
|
||||
if ((fallbackProperties.optimalTilingFeatures &
|
||||
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
|
||||
@@ -309,7 +310,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
|
||||
}
|
||||
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
|
||||
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
|
||||
if (fallbackLogicalFormat &&
|
||||
HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
|
||||
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
|
||||
}
|
||||
}
|
||||
@@ -326,31 +328,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
|
||||
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
|
||||
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
|
||||
const MG_External::VulkanCapabilities& capabilities,
|
||||
FormatCapabilityCache& cache) {
|
||||
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() {
|
||||
// P12 review fix: A SERVER SESSION THAT ENDS WITHOUT A CLEAN TEARDOWN LEAVES NO RENDERER ON
|
||||
// THE WINDOW. The renderer - swapchain and VkSurfaceKHR on the window - is a process global
|
||||
// that only ReleaseEGLResources / a released surface drops, and a client that went away
|
||||
// without eglTerminate (a crash, a kill, a dropped connection) sends neither. Under a
|
||||
// transport this object is the server's and its destruction is the session's end (ServerLoop's
|
||||
// apply thread, after the final drain, before the display lease ends), so the renderer goes
|
||||
// here: nothing of the ended session keeps presenting into, or holding a surface on, the
|
||||
// server's window once its lease is over. Monolith keeps its renderer.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && pVulkanRenderer != nullptr) {
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
}
|
||||
#else
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||
#endif
|
||||
|
||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
|
||||
|
||||
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
||||
if (!m_windowHandle.Handle) {
|
||||
@@ -383,18 +368,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::Initialize() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager::RegisterWireResourceOps();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). BESIDE the resource ops and for the same
|
||||
// reason: this is the one place both Magma server roles pass through - the inproc
|
||||
// role and the spawn/TCP session child - and ServerLoop::CreateBackend calls it at
|
||||
// step 1 of InitServerRoleCommon, before the client session exists and therefore
|
||||
// before any frontend object can die unheard. A per-transport install is how one of
|
||||
// the two roles ends up without it.
|
||||
InstallStateObjectDeathOps();
|
||||
#endif
|
||||
#endif
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
@@ -416,9 +389,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -439,8 +409,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_E("DirectVulkan backend not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
||||
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
|
||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
|
||||
handle.Backend != WindowBackend::X11 &&
|
||||
handle.Backend != WindowBackend::MetalLayer &&
|
||||
handle.Backend != WindowBackend::Win32)) {
|
||||
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||
return false;
|
||||
}
|
||||
@@ -497,9 +469,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
@@ -509,9 +478,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
||||
@@ -531,158 +497,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
.IsCompatibilityProfile = false
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location,
|
||||
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||
// version: a caller that finds the extension missing never resolves
|
||||
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||
// blocks anyway calls through a null pointer.
|
||||
E_GL_ARB_uniform_buffer_object,
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
|
||||
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
|
||||
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
|
||||
// every immutable-storage texture (see the comment there).
|
||||
E_GL_ARB_texture_view,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
|
||||
// now reaches the shader on BOTH backends - Magma resolves the lowered
|
||||
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
|
||||
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
|
||||
// landed, because the counter silently read whatever was bound as SSBO N instead.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it. Kept identical to the DirectGLES block so the two
|
||||
// backends do not disagree about what MobileGL is.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports.
|
||||
E_GL_ARB_viewport_array,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||
// device's: the compiler threads belong to MobileGL's shader pool and
|
||||
// glCompileShader/glLinkProgram are serviced entirely inside the frontend, so there
|
||||
// is no device feature to condition this on.
|
||||
//
|
||||
// Gated on the async flag deliberately, and this is the whole reason the gate
|
||||
// exists. Advertising the string is the one part of asynchronous compilation that a
|
||||
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
|
||||
// the moment they see it - they enqueue whole pipeline batches and poll
|
||||
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
|
||||
// change as well as the threading, or the kill switch would only be half a switch.
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
|
||||
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
|
||||
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
|
||||
// so an application that simply uses doubles needs nothing advertised. What the extension
|
||||
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
|
||||
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
|
||||
// decision about all of it rather than about the shader path alone.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
||||
// only advertised when the device actually supports timestamp queries and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -696,18 +539,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
|
||||
// view type cannot be created without the imageCubeArray device feature - so the string
|
||||
// follows the feature, not the version, exactly as the per-layer attachment bit does.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -754,8 +585,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
@@ -771,6 +600,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
@@ -791,15 +628,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries share the handle-based result/delete entries, which must
|
||||
// exist even when timer queries are disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -814,9 +642,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -827,17 +652,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||
// extension by itself on devices with no native subgroup support at all; a
|
||||
// device with native subgroups always advertises - and uses - those.
|
||||
const Bool subgroupSupportAdvertised =
|
||||
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
|
||||
m_vulkanCaps.SupportsImageCubeArray);
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -885,14 +702,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
// rather than a maximum the sampler manager will never apply.
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
||||
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
||||
: 1.0f;
|
||||
: 1.0f;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
||||
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
||||
@@ -913,7 +729,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
|
||||
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
|
||||
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
|
||||
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
const Int maxSupportedTextureUnits =
|
||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
|
||||
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
|
||||
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
|
||||
@@ -923,7 +740,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// limits while keeping the combined limit at our texture-unit array capacity.
|
||||
constexpr Int maxPerStageTextureUnits =
|
||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
|
||||
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
|
||||
m_dynamicParameters.MaxTextureImageUnits =
|
||||
std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
|
||||
m_dynamicParameters.MaxVertexTextureImageUnits =
|
||||
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
|
||||
m_dynamicParameters.MaxComputeTextureImageUnits =
|
||||
@@ -932,93 +750,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
|
||||
// Never advertise more attributes than the state layer can store: the current-value array and
|
||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
|
||||
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
|
||||
// queries at descriptor-indexing scale - the same driver whose
|
||||
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
|
||||
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
|
||||
// created that many buffers and spliced that many UBO declarations into a single compute
|
||||
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
|
||||
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
|
||||
// discriminator. Every ceiling below is far above what any desktop driver advertises for
|
||||
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
|
||||
// can only lower a limit that was never usable in the first place. The zero floor is not
|
||||
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
|
||||
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
|
||||
const Int clamped = std::min(std::max(reported, 0), ceiling);
|
||||
if (clamped != reported) {
|
||||
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
|
||||
}
|
||||
return clamped;
|
||||
};
|
||||
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
|
||||
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
|
||||
constexpr Int kMaxAdvertisedBufferBlocks = 256;
|
||||
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
|
||||
m_dynamicParameters.MaxComputeShaderStorageBlocks =
|
||||
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
|
||||
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeUniformBlocks =
|
||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxVertexAttribs =
|
||||
std::min(m_vulkanCaps.MaxVertexAttribs,
|
||||
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
|
||||
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
|
||||
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
|
||||
// them. Not clamped: unlike the block counts these are not amounts an application
|
||||
// allocates, and the frontend already raises them to the GL minimum.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
|
||||
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
|
||||
// carries), so the stage limits differ only by whether the stage can have blocks at all.
|
||||
//
|
||||
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
|
||||
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
|
||||
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
|
||||
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
|
||||
// through this backend - those write their result through a storage block declared in a
|
||||
// tessellation stage. Gating would report 0 and turn 433 passing cases into
|
||||
// "unsupported", removing function that demonstrably works.
|
||||
//
|
||||
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
|
||||
// the driver refuses outright at link time; the honest limit converts a silent
|
||||
// wrong-render into a capability an application can route around. Here there is no such
|
||||
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
|
||||
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
|
||||
// pipeline creation where the rejection is observable, not on a feature bit this driver
|
||||
// reports inaccurately.
|
||||
{
|
||||
const Int maxPerStageStorageBlocks =
|
||||
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
|
||||
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
||||
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
|
||||
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
// The one hard capability in the set: no geometry stage means no blocks in it.
|
||||
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
||||
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
|
||||
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
}
|
||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||
const Int maxPerStageImageUniforms =
|
||||
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
||||
@@ -1035,156 +779,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
||||
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
const Int maxSupportedDrawBuffers =
|
||||
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
||||
// Same shape as the image-uniform limits three lines above: maxClipDistances is reported
|
||||
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
|
||||
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
|
||||
// it the limit describes a capacity no shader may use, so report none.
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||
// The cull pair, gated on its own feature. shaderCullDistance is separate from
|
||||
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
|
||||
// gate rather than riding on the clip one.
|
||||
m_dynamicParameters.MaxCullDistances =
|
||||
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
|
||||
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
|
||||
// with only one of the two features must not report a combined capacity that implies the
|
||||
// other, so the gate is "either feature" and the value never drops below what is enabled.
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
|
||||
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
|
||||
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
|
||||
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
|
||||
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
|
||||
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
||||
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
|
||||
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_vulkanCaps.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
||||
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
|
||||
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
|
||||
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
|
||||
// layered targets are declared separately as their own machinery lands.
|
||||
{
|
||||
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
|
||||
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
|
||||
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
|
||||
// image can name. Optimistic: a format that refuses the flag is caught at image creation
|
||||
// and declines the slice view there, which the clear path handles as a soft miss.
|
||||
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
|
||||
}
|
||||
if (m_vulkanCaps.SupportsImageCubeArray) {
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
// The device feature the whole fp64 story hangs off. With it, a module keeps its
|
||||
// OpCapability Float64 and real doubles reach the driver; without it the transpile
|
||||
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
|
||||
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||
// detected answer's declines.
|
||||
{
|
||||
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
supportsStagePointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the device feature");
|
||||
supportsStagePointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||
}
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
|
||||
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
|
||||
// bitcast it back.
|
||||
//
|
||||
// Re-coupling it does not work, and the reason is worth recording because it is not
|
||||
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
|
||||
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
|
||||
// `in vec4` is not only legal but the common case
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
|
||||
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
|
||||
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
|
||||
// narrowing OFF for every one of them and the attributes come back unfetched.
|
||||
//
|
||||
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
|
||||
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
|
||||
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
|
||||
// ShaderCompiler::SanitizeAndOptimizeBinary.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
|
||||
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
|
||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||
// advertised values describe the 32-lane virtual subgroup the compute
|
||||
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||
// GL requires the advertisement and the execution to agree, and on this
|
||||
// path the emulation is what executes; only the compute stage is offered.
|
||||
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
||||
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||
} else {
|
||||
m_dynamicParameters.SubgroupSize = 0;
|
||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||
@@ -1193,7 +806,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
|
||||
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
|
||||
m_vulkanCaps.MaxShaderStorageBlockSize, m_dynamicParameters.MaxShaderStorageBlockSize);
|
||||
m_vulkanCaps.MaxShaderStorageBlockSize,
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize);
|
||||
}
|
||||
switch (m_vulkanCaps.VendorId) {
|
||||
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
||||
|
||||
@@ -62,21 +62,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,34 +23,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
|
||||
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
|
||||
// non-null on every Magma role that owns a backend. See the definition for what the one
|
||||
// arm does and for the inproc gap it closes. Push-only and disaggregated-only: a pull
|
||||
// build declares no notice at all (StateObjectDeathNotice.h) and has nothing to emit.
|
||||
void InstallStateObjectDeathOps();
|
||||
// True when the notice currently installed is this backend's. Read by MG_Test so the
|
||||
// install can be asserted without a death having to happen first.
|
||||
Bool StateObjectDeathOpsInstalled();
|
||||
#endif
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
// safe because GL calls are serialized in this codebase, and any still-live
|
||||
// program rebuilds its entry from the retained generated SPIR-V on demand.
|
||||
void ClearProgramResourceCaches();
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLuint* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void Clear(GLbitfield mask);
|
||||
@@ -94,9 +72,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -107,7 +85,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
||||
GLsizei* length, GLchar* name);
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
@@ -129,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only while a live renderer exists whose device can actually time.
|
||||
Bool IsTimerQuerySupported();
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "BufferArena.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
|
||||
@@ -114,11 +111,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (buffer.IsValid()) {
|
||||
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
|
||||
// still names it, and those slices stay in service until the frame slot is rewound
|
||||
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
|
||||
// memos). The release therefore has to survive every mid-frame reclaim and land on
|
||||
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
|
||||
m_deferredReleases[frameIndex].push_back(std::move(buffer));
|
||||
}
|
||||
|
||||
@@ -128,14 +120,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferDesc.usage = m_desc.usage;
|
||||
bufferDesc.memoryUsage = m_desc.memoryUsage;
|
||||
bufferDesc.allocationFlags = m_desc.allocationFlags;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Split staging may also be the destination of a GPU UBO range copy.
|
||||
// Persistent host writes must be coherent: Upload deliberately writes
|
||||
// the mapped pointer without a per-range VMA flush.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
|
||||
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
}
|
||||
#endif
|
||||
if (!buffer.Create(bufferDesc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "FrameContext.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
|
||||
@@ -19,19 +16,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = device;
|
||||
m_commandPool = commandPool;
|
||||
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = frameCount * 2;
|
||||
allocInfo.commandBufferCount = frameCount;
|
||||
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
m_frames[i].commandBuffer = commandBuffers[i];
|
||||
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
|
||||
}
|
||||
|
||||
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
@@ -51,10 +47,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
||||
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
commandBuffers[i] = m_frames[i].commandBuffer;
|
||||
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
|
||||
}
|
||||
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
@@ -65,7 +60,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
|
||||
}
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
@@ -92,8 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
||||
GetCurrent().isCommandRecording = false;
|
||||
GetCurrent().hasCommandBufferRecorded = false;
|
||||
GetCurrent().isPreCommandRecording = false;
|
||||
GetCurrent().hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
||||
@@ -125,41 +118,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
|
||||
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
|
||||
void FrameContext::EndPreCommandRecordingIfOpen() {
|
||||
auto& frame = GetCurrent();
|
||||
if (!frame.isPreCommandRecording) {
|
||||
return;
|
||||
}
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
void FrameContext::AbandonPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
|
||||
}
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
|
||||
DestroySwapchainSemaphores(device);
|
||||
if (swapchainImageCount == 0) {
|
||||
@@ -192,41 +150,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
|
||||
auto& frame = GetCurrent();
|
||||
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
|
||||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The barrier belongs in the frame's own recording. Bailing out because
|
||||
// something was already recorded (the previous behaviour) dropped the
|
||||
// transition entirely for every frame that never ran a default-framebuffer
|
||||
// render pass - the only other thing that carries the image to
|
||||
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
|
||||
// handed to the WSI still in the layout it was acquired in.
|
||||
// A closed-but-unsubmitted buffer can only come from a submit that already
|
||||
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
|
||||
// appending to it is illegal while reopening would reset the frame's own
|
||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||
// rather than trade a lost device for a barrier into a closed buffer.
|
||||
if (frame.hasCommandBufferRecorded) {
|
||||
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reopening a recording here would vkResetCommandBuffer this frame's own
|
||||
// commands away, so append to the open one and let the caller close it.
|
||||
const Bool openedRecording = !frame.isCommandRecording;
|
||||
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
|
||||
auto& commandBuffer = BeginCommandRecording();
|
||||
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
presentBarrier.srcAccessMask = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A layout transition writes the image too. Order it after the wire
|
||||
// render pass's storeOp, clears and copies instead of relying on the old
|
||||
// per-draw queue-idle path. This applies even when contents are discarded.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
#endif
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -238,19 +171,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
presentBarrier.subresourceRange.levelCount = 1;
|
||||
presentBarrier.subresourceRange.baseArrayLayer = 0;
|
||||
presentBarrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#else
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#endif
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
if (openedRecording) {
|
||||
EndCommandRecording();
|
||||
}
|
||||
EndCommandRecording();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,27 +182,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 swapchainImageIndex) const {
|
||||
const auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"GetSubmitInfo called while the pre-pass stream is still recording");
|
||||
AssertValidSwapchainImageIndex(swapchainImageIndex);
|
||||
SubmitInfoPacket packet{};
|
||||
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
||||
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
|
||||
|
||||
Uint32 commandBufferCount = 0;
|
||||
// The pre-pass stream executes strictly before the frame's commands.
|
||||
if (frame.hasPreCommandBufferRecorded) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
|
||||
}
|
||||
if (shouldSubmitCommandBuffer) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
|
||||
}
|
||||
packet.commandBuffer = frame.commandBuffer;
|
||||
|
||||
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
|
||||
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
||||
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
||||
packet.submitInfo.commandBufferCount = commandBufferCount;
|
||||
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
|
||||
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
|
||||
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
|
||||
packet.submitInfo.signalSemaphoreCount = 1;
|
||||
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
||||
return packet;
|
||||
@@ -307,29 +221,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
// Under disaggregation VulkanRenderer waits every registered fence
|
||||
// through this slot's last submit before calling here. That aggregate
|
||||
// proof covers mid-frame pooled-fence flushes too; the slot fence alone
|
||||
// would not. Initialize's first acquire has no prior submissions.
|
||||
// The slot's fence has been waited: every command buffer this slot
|
||||
// submitted (including mid-frame flushes) has finished executing.
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
|
||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||
&outImageIndex);
|
||||
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
|
||||
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
|
||||
// the consumed-flag reset (leaving a stale "already consumed", so the next
|
||||
// submit never waited on the pending signal) and the fence reset (leaving
|
||||
// the slot's fence signaled for the next submit to reuse). Only a genuine
|
||||
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
|
||||
// and nothing is signaled - skips the bookkeeping.
|
||||
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
||||
return vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
}
|
||||
|
||||
Uint32 FrameContext::GetCurrentFrameIndex() const {
|
||||
@@ -344,14 +247,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_recordingObserver = observer;
|
||||
}
|
||||
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer() {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
||||
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
||||
auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the command buffer is still recording");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
@@ -359,23 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
frame.preCommandBuffer = preReplacement;
|
||||
}
|
||||
// lastSubmitIndex was just written by the renderer for the submission
|
||||
// that carried this command buffer.
|
||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
||||
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
|
||||
frame.commandBuffer = replacement;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
@@ -385,40 +274,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
||||
for (const auto& retired : frame.retiredCommandBuffers) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
|
||||
}
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
|
||||
frame.retiredCommandBuffers.data());
|
||||
}
|
||||
frame.retiredCommandBuffers.clear();
|
||||
}
|
||||
|
||||
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
|
||||
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
for (auto& frame : m_frames) {
|
||||
// Retired buffers are appended in submit order, so the completed
|
||||
// ones form a prefix.
|
||||
SizeT completedCount = 0;
|
||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
++completedCount;
|
||||
}
|
||||
if (completedCount > 0) {
|
||||
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
|
||||
frame.retiredCommandBuffers.begin() + completedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::FreeAllRetiredCommandBuffers() {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
// command buffer; submitInfo.pCommandBuffers points here.
|
||||
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
};
|
||||
|
||||
@@ -42,35 +40,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||
};
|
||||
|
||||
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
|
||||
// with the submit-tracker index it was submitted under so it can be
|
||||
// freed as soon as that submission is observed complete - without
|
||||
// waiting for the slot's fence to be waited again (present-less flush
|
||||
// loops never wait it).
|
||||
struct RetiredCommandBuffer {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Uint64 submitIndex = 0;
|
||||
};
|
||||
|
||||
struct FrameData {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// Pre-pass work stream: out-of-pass commands (deferred clear
|
||||
// materialization, sampled-layout transitions) for resources the
|
||||
// frame's recording has not touched yet. Submitted immediately
|
||||
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
|
||||
// into it never has to split the frame's active render pass.
|
||||
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
|
||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
||||
Bool isCommandRecording = false;
|
||||
Bool hasCommandBufferRecorded = false;
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
// complete (fence wait or completion poll).
|
||||
Vector<RetiredCommandBuffer> retiredCommandBuffers;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands) whose
|
||||
// execution is only known complete once this slot's fence has been
|
||||
// waited again; freed at that point.
|
||||
Vector<VkCommandBuffer> retiredCommandBuffers;
|
||||
// Submit-tracker index of this slot's most recent queue submission
|
||||
// (written by the renderer at submit time).
|
||||
Uint64 lastSubmitIndex = 0;
|
||||
@@ -87,14 +67,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
||||
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
||||
void EndCommandRecording();
|
||||
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
|
||||
VkCommandBuffer BeginPreCommandRecording();
|
||||
// Closes the pre stream if open, marking it for submission ahead of the
|
||||
// frame command buffer. Safe to call when it never opened.
|
||||
void EndPreCommandRecordingIfOpen();
|
||||
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
|
||||
// frame recordings, swapchain recreation).
|
||||
void AbandonPreCommandRecording();
|
||||
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
|
||||
void DestroySwapchainSemaphores(VkDevice device);
|
||||
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
|
||||
@@ -107,18 +79,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Parks the current (already ended and submitted) command buffer on the
|
||||
// slot's retired list and installs a freshly allocated one, so recording
|
||||
// can restart while the submitted buffer is still executing. Retired
|
||||
// buffers are freed after the slot's fence is next waited, or as soon
|
||||
// as their submission is observed complete.
|
||||
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
|
||||
|
||||
// Frees every retired command buffer whose tagged submission index is
|
||||
// known complete. Driven by the renderer's submit tracker on completion
|
||||
// events (fence waits and non-blocking polls), so present-less flush
|
||||
// loops reclaim their buffers without any extra wait.
|
||||
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
|
||||
// Frees every slot's retired command buffers. Only valid when the
|
||||
// caller has proven every queue submission complete.
|
||||
void FreeAllRetiredCommandBuffers();
|
||||
// buffers are freed after the slot's fence is next waited.
|
||||
VkResult RetireCurrentCommandBuffer();
|
||||
|
||||
Uint32 GetCurrentFrameIndex() const;
|
||||
Uint32 GetFrameCount() const;
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
||||
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
||||
// graph is unchanged (G1).
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||
// {slot, gen} mint the re-keyed sites are written against.
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
|
||||
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
|
||||
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
|
||||
//
|
||||
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
|
||||
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
|
||||
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
|
||||
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
|
||||
//
|
||||
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
|
||||
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
|
||||
// nothing outside Track H consults the legacy-memo lever at all - see
|
||||
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
|
||||
//
|
||||
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
|
||||
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
||||
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
||||
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D14's startup gate
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
|
||||
// that is actually running. It answers exactly one question and it answers it before the
|
||||
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
|
||||
//
|
||||
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
|
||||
//
|
||||
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
|
||||
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
||||
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
||||
// not kill an Espryt one.
|
||||
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
|
||||
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
|
||||
// a clear bit there simply means the client is not pushing render-state CSOs in this
|
||||
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
||||
// and 6 "Track H" and labels bit 0 nothing of the sort.
|
||||
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
||||
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
||||
// branch nobody needs on the hottest path in the backend.
|
||||
//
|
||||
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
|
||||
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
|
||||
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
|
||||
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
|
||||
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
|
||||
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
|
||||
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
||||
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
||||
inline void MagmaPipeValidateSubsystemConfiguration() {
|
||||
if (!MG_Config::Features.PipeLegacyMemos &&
|
||||
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
||||
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
||||
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
||||
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
|
||||
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
|
||||
"to measure it.",
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemRenderState));
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
||||
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||
if (MG_Config::Features.PipeLegacyMemos) return;
|
||||
#endif
|
||||
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
||||
#else
|
||||
const char* const why =
|
||||
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
||||
#endif
|
||||
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
||||
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
||||
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
||||
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
|
||||
why,
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return MagmaPipeSubsystemOn(trackHBit);
|
||||
#else
|
||||
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||
// running process can be on.
|
||||
(void)trackHBit;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||
// and a control that defeats two of three guards proves nothing.
|
||||
//
|
||||
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||
//
|
||||
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||
//
|
||||
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||
// which is exactly the vacuity this control exists to catch.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||
//
|
||||
// * THIS MINT still has no death notification, and that is now a narrower statement than
|
||||
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
|
||||
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
|
||||
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
|
||||
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
|
||||
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
|
||||
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
|
||||
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
|
||||
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
|
||||
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
|
||||
// from that is unchanged;
|
||||
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||
// across frames by design. The two requirements are mutually exclusive, so the
|
||||
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||
//
|
||||
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||
// suite; it is the only place in the tree where that deletion is caught.
|
||||
//
|
||||
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||
// "every safety net was switched off until something broke".
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||
//
|
||||
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||
// and with no `default:` there is no arm for it to read true from.
|
||||
//
|
||||
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||
// and could not make anything red or green.)
|
||||
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||
switch (kind) {
|
||||
// The two MagmaPipeIdentityTables really mints.
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
case MG_Pipe::MGPipeKind::Buffer:
|
||||
return true;
|
||||
// P4a's six object classes: still reached from their frontend objects on this
|
||||
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||
// the knob to defeat.
|
||||
case MG_Pipe::MGPipeKind::Texture:
|
||||
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||
// ...and everything else this backend does not mint a handle for.
|
||||
case MG_Pipe::MGPipeKind::None:
|
||||
case MG_Pipe::MGPipeKind::Xfb:
|
||||
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||
case MG_Pipe::MGPipeKind::Fence:
|
||||
case MG_Pipe::MGPipeKind::Query:
|
||||
case MG_Pipe::MGPipeKind::Context:
|
||||
case MG_Pipe::MGPipeKind::KindCount:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||
"defeat for it");
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||
"carries the answer into the lane");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The {slot, gen} mint
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||
// properties, and the third is the one review v2 got wrong:
|
||||
//
|
||||
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||
// never match a live object even if the allocator hands back the same heap address
|
||||
// (the ABA HandleRecycleScenario reproduces);
|
||||
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||
// no probe and no mix;
|
||||
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||
// drawn, whatever the working set size.
|
||||
//
|
||||
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||
//
|
||||
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||
//
|
||||
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||
// plus one map node per object EVER created, for the life of the process, on a
|
||||
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||
// ~1024 frames, never to a hot one.
|
||||
//
|
||||
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||
// no capacity to size off a device measurement because there is no capacity; what the
|
||||
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||
// draw - ROADMAP.md:7).
|
||||
//
|
||||
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||
// clock, across two live contexts.
|
||||
class MagmaPipeIdentityTable {
|
||||
public:
|
||||
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||
|
||||
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||
// Objects currently holding a slot - the live working set this table tracks.
|
||||
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||
|
||||
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||
// that a zero can never be minted into a slot and then indexed with.
|
||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||
// that has been handed back to the free list.
|
||||
if (lifetimeId == m_lastLifetimeId) {
|
||||
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||
return m_lastHandle;
|
||||
}
|
||||
Uint32 index = 0;
|
||||
const auto it = m_index.find(lifetimeId);
|
||||
if (it != m_index.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = ClaimSlot();
|
||||
m_entries[index].LifetimeId = lifetimeId;
|
||||
m_index.emplace(lifetimeId, index);
|
||||
}
|
||||
Entry& entry = m_entries[index];
|
||||
entry.LastUse = m_boundary;
|
||||
m_lastLifetimeId = lifetimeId;
|
||||
m_lastIndex = index;
|
||||
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||
entry.Gen};
|
||||
return m_lastHandle;
|
||||
}
|
||||
|
||||
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||
// slots' consumers use.
|
||||
void OnFrameBoundary() {
|
||||
++m_boundary;
|
||||
if ((m_boundary % kSweepInterval) != 0) return;
|
||||
SizeT retired = 0;
|
||||
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||
Entry& entry = m_entries[it->second];
|
||||
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||
entry.LifetimeId = 0;
|
||||
m_freeSlots.push_back(it->second);
|
||||
it = m_index.erase(it);
|
||||
++retired;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (retired != 0) {
|
||||
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||
// so a front memo pointing at one would still hand out a handle the consumer
|
||||
// tables would accept. Drop it.
|
||||
m_lastLifetimeId = 0;
|
||||
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||
m_kindName, retired, LiveCount(), Count());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Sweep cadence and retirement age, deliberately the same numbers
|
||||
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||
// entry is still live and still correct, which is a pure waste.
|
||||
static constexpr Uint64 kSweepInterval = 256;
|
||||
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
|
||||
struct Entry {
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 LastUse = 0;
|
||||
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||
// its generation, which is what makes a memo survive a reconfiguration.
|
||||
Uint32 Gen = 0;
|
||||
};
|
||||
|
||||
Uint32 ClaimSlot() {
|
||||
while (!m_freeSlots.empty()) {
|
||||
const Uint32 index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||
// stale handle match a live object. It costs one slot.
|
||||
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||
"retired for good; {slot, gen} stays unique",
|
||||
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
continue;
|
||||
}
|
||||
++m_entries[index].Gen;
|
||||
return index;
|
||||
}
|
||||
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||
m_entries.push_back(Entry{});
|
||||
m_entries[index].Gen = 1;
|
||||
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||
//
|
||||
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||
// and it cannot read a line that was compiled away.
|
||||
const SizeT minted = m_entries.size();
|
||||
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||
m_kindName, minted, LiveCount());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* m_kindName = "";
|
||||
Uint64 m_boundary = 0;
|
||||
Vector<Entry> m_entries;
|
||||
Vector<Uint32> m_freeSlots;
|
||||
UnorderedMap<Uint64, Uint32> m_index;
|
||||
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||
// object's lifetime id is never 0.
|
||||
Uint64 m_lastLifetimeId = 0;
|
||||
Uint32 m_lastIndex = 0;
|
||||
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||
class MagmaPipeIdentityTables {
|
||||
public:
|
||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||
: m_vaos.Acquire(lifetimeId);
|
||||
}
|
||||
void OnFrameBoundary() {
|
||||
m_vaos.OnFrameBoundary();
|
||||
m_buffers.OnFrameBoundary();
|
||||
}
|
||||
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||
|
||||
private:
|
||||
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||
};
|
||||
|
||||
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||
//
|
||||
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||
? 0u
|
||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||
}
|
||||
|
||||
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||
//
|
||||
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||
// it replaces gave, without the fixed capacity.
|
||||
template <typename T, Uint32 kChunkEntries = 256>
|
||||
class MagmaPipeSlotTable {
|
||||
public:
|
||||
T& operator[](Uint32 index) {
|
||||
const Uint32 chunk = index / kChunkEntries;
|
||||
while (m_chunks.size() <= chunk) {
|
||||
m_chunks.push_back(MakeUnique<Chunk>());
|
||||
}
|
||||
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||
}
|
||||
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
T Entries[kChunkEntries] = {};
|
||||
};
|
||||
Vector<UniquePtr<Chunk>> m_chunks;
|
||||
};
|
||||
|
||||
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||
// this code rather than a copy of it.
|
||||
//
|
||||
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||
//
|
||||
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||
// slot" and "the slot was reused and Gen did not move".
|
||||
//
|
||||
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||
template <typename Memos, Uint32 kChunkEntries>
|
||||
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||
const MG_Pipe::MGPipeHandle& handle) {
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
return table[kMagmaPipeAbaControlSlotIndex];
|
||||
}
|
||||
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||
if (!(memos.Owner == handle)) {
|
||||
memos = Memos{};
|
||||
memos.Owner = handle;
|
||||
}
|
||||
return memos;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,224 +0,0 @@
|
||||
// MobileGL - server-owned program input for Magma (P5f fm).
|
||||
#pragma once
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include "../DirectVulkanResourceState.h"
|
||||
// P7 wave 2 package C, OQ-8: spirv_reflect.h is GONE from this header, and its absence is the
|
||||
// gate. Everything this class needed it for - the storage-block index space - now travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
|
||||
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
|
||||
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A borrowed view, valid only while its frontend object or server record lives.
|
||||
// It never constructs a ProgramObject on the server. Post-link mutable values
|
||||
// come from the record tails, not from the archive's link-time defaults.
|
||||
class MagmaProgramSource {
|
||||
public:
|
||||
using Program = MG_State::GLState::ProgramObject;
|
||||
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
|
||||
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
|
||||
: m_handle(handle), m_record(&record) {
|
||||
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
|
||||
}
|
||||
|
||||
Bool IsWire() const { return m_record != nullptr; }
|
||||
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
|
||||
const Program* Frontend() const { return m_frontend; }
|
||||
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
}
|
||||
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
|
||||
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
|
||||
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
}
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
|
||||
Vector<ShaderStage> result;
|
||||
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
|
||||
return result;
|
||||
}
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
|
||||
const auto& stages = m_record->Archive->LinkedStages;
|
||||
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
|
||||
}
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
|
||||
// A view has no lifetime in which a memo can safely persist. The server
|
||||
// factory's content cache still owns compiled programs; hash from current
|
||||
// archive + binding tails rather than borrowing the client's mutable memo.
|
||||
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
|
||||
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
|
||||
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
|
||||
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
|
||||
|
||||
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
|
||||
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
|
||||
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
}
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
|
||||
auto found = Link().uniformBlockIndexByName.find(name);
|
||||
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
|
||||
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
|
||||
}
|
||||
Uint GetUniformBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
|
||||
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
|
||||
const auto* block = Block(index);
|
||||
static const String empty;
|
||||
return block ? block->name : empty;
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
|
||||
const auto* block = Block(index);
|
||||
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
|
||||
}
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
|
||||
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
|
||||
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformType(location);
|
||||
return IsValidUniformLocation(static_cast<Int>(location))
|
||||
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
|
||||
}
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const auto& units = m_record->SamplerUnits;
|
||||
const auto found = std::lower_bound(units.begin(), units.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
|
||||
return found != units.end() && found->Location == location ? found->Unit : -1;
|
||||
}
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformLocation(name);
|
||||
const auto& locations = Link().uniformLocations;
|
||||
auto found = locations.find(name);
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.empty()) return -1;
|
||||
found = locations.find(name + "[0]");
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.back() != ']' || name.size() < 4) return -1;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.size() - 1; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return -1;
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0fffffffu) return -1;
|
||||
}
|
||||
found = locations.find(name.substr(0, bracket) + "[0]");
|
||||
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
|
||||
if (found == locations.end()) return -1;
|
||||
const Int base = static_cast<Int>(found->second);
|
||||
if (!IsValidUniformLocation(base)) return -1;
|
||||
const Int tIndex = Link().uniformIndexInTProgram[base];
|
||||
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
|
||||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
|
||||
const Int location = base + static_cast<Int>(element);
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
Uint32 GetTransformFeedbackStride(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
|
||||
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const {
|
||||
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
|
||||
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
|
||||
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
|
||||
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
|
||||
// function of the linked program, so it is now built once at link time and travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
|
||||
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
|
||||
Uint GetShaderStorageBlockIndex(const String& name) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
// The archive carries NORMALISED names (the array subscript stripped), because
|
||||
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
|
||||
// ProgramFactory::ReflectLayout looks up. A caller that still holds a GL-style
|
||||
// "Foo[2]" gets the same second chance it always had.
|
||||
const auto find = [&blocks](const String& key) {
|
||||
return std::find_if(blocks.begin(), blocks.end(),
|
||||
[&](const MG_State::GLState::StorageBlockReflection& block) {
|
||||
return block.name == key;
|
||||
});
|
||||
};
|
||||
auto found = find(name);
|
||||
if (found == blocks.end() && !name.empty() && name.back() == ']') {
|
||||
const auto bracket = name.rfind('[');
|
||||
if (bracket != String::npos) found = find(name.substr(0, bracket));
|
||||
}
|
||||
return found == blocks.end() ? GL_INVALID_INDEX
|
||||
: static_cast<Uint>(found - blocks.begin());
|
||||
}
|
||||
Uint GetShaderStorageBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
if (index >= blocks.size()) return 0;
|
||||
const auto& block = blocks[index];
|
||||
// THE OVERRIDE STILL WINS, and it still comes off the RECORD rather than the
|
||||
// archive: glShaderStorageBlockBinding is a post-link mutable and the archive
|
||||
// carries link-time defaults only (rule on MagmaProgramSource, and the reason
|
||||
// StorageBlockReflection::binding is documented as the DECLARED binding).
|
||||
for (const auto& override : m_record->StorageOverrides) {
|
||||
if (override.Name == block.name) return override.Binding;
|
||||
}
|
||||
return block.binding;
|
||||
}
|
||||
|
||||
private:
|
||||
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
|
||||
const MG_State::GLState::BlockReflection* Block(Uint index) const {
|
||||
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
|
||||
const Int tIndex = Link().glBlockIndexToTProgram[index];
|
||||
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
|
||||
? &Link().blockReflection[tIndex] : nullptr;
|
||||
}
|
||||
const Program* m_frontend = nullptr;
|
||||
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
|
||||
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
|
||||
// were `mutable` precisely because this class is a borrowed const view - and that is
|
||||
// also why they never helped: a view lives for one draw, so the latch was cold every
|
||||
// time it was read.
|
||||
};
|
||||
#else
|
||||
using MagmaProgramSource = MG_State::GLState::ProgramObject;
|
||||
#endif
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
@@ -198,35 +197,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (payload.wireRenderPassCompatibilityId != 0) {
|
||||
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
|
||||
sizeof(payload.wireRenderPassCompatibilityId)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
@@ -263,132 +243,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const HashType hash = ComputeHash(payload);
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second.pipeline;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VkPipeline pipeline = CreatePipeline(payload);
|
||||
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
|
||||
// the rest of the process, so one transient driver rejection turned every later draw with
|
||||
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
|
||||
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
|
||||
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
||||
// skipped.
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
|
||||
// assembled and the driver refused is a broken invariant, not an expected failure,
|
||||
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
|
||||
// Log.h ordering fix made MGLOG_E live in INFO builds.
|
||||
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||
"programHash=0x%llx; not caching the failure",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(payload.programHash));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Wire pipelines outlive their creator pass and
|
||||
// may serve another compatible pass in flight.
|
||||
// Never evict them by a recycled native handle.
|
||||
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
|
||||
#endif
|
||||
payload.renderPass,
|
||||
m_frameCounter});
|
||||
m_cache.emplace(hash, pipeline);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void PipelineFactory::DestroyAll() {
|
||||
for (auto& pair : m_cache) {
|
||||
if (pair.second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
|
||||
if (pair.second != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second, nullptr);
|
||||
}
|
||||
}
|
||||
m_cache.clear();
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::OnFrameBoundary() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
|
||||
// pipeline" memo when this returns non-zero: the memo can return a cached
|
||||
// handle without touching this cache, so an evicted pipeline may still be
|
||||
// memoized (present-less flush loops never reset the memo per frame).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
|
||||
m_cache.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (it->second.programHash == programHash) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
|
||||
evicted, static_cast<unsigned long long>(programHash));
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||
@@ -423,18 +294,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ia.topology = payload.topology;
|
||||
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
|
||||
// otherwise is what the spec expects.
|
||||
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
@@ -443,28 +305,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
||||
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
||||
raster.lineWidth = 1.0f;
|
||||
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
|
||||
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
|
||||
// selector already collapses to FIRST in exactly that case - so a device without the
|
||||
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
|
||||
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
|
||||
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
|
||||
provokingVertexState.pNext = raster.pNext;
|
||||
raster.pNext = &provokingVertexState;
|
||||
}
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
@@ -510,60 +353,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
blend.attachmentCount = payload.colorAttachmentCount;
|
||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
||||
|
||||
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
|
||||
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
|
||||
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
|
||||
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
|
||||
//
|
||||
// The refusal below is what keeps the half-tessellated shape away from the driver when
|
||||
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
|
||||
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
|
||||
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
|
||||
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
stagesWithPassthrough = *payload.stages;
|
||||
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||
effectiveStages = &stagesWithPassthrough;
|
||||
}
|
||||
{
|
||||
VkShaderStageFlags stagesPresent = 0;
|
||||
for (const auto& stageInfo : *effectiveStages) {
|
||||
stagesPresent |= stageInfo.stage;
|
||||
}
|
||||
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
|
||||
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
|
||||
if (hasTessControl != hasTessEval) {
|
||||
// Latched, and the latch is the point: a failed creation is deliberately never
|
||||
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
|
||||
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
|
||||
// noise, not a diagnostic. One line names the program; the draws it explains are
|
||||
// all the same draw.
|
||||
static Bool s_warnedHalfTessellatedPipeline = false;
|
||||
if (!s_warnedHalfTessellatedPipeline) {
|
||||
s_warnedHalfTessellatedPipeline = true;
|
||||
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
|
||||
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
|
||||
"patchControlPoints=%u. Its draws are skipped; logged once.",
|
||||
hasTessEval ? "an evaluation" : "a control",
|
||||
hasTessEval ? "control" : "evaluation",
|
||||
static_cast<unsigned long long>(payload.programHash),
|
||||
payload.patchControlPoints);
|
||||
}
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||
gpi.pStages = effectiveStages->data();
|
||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
|
||||
gpi.pViewportState = &vpci;
|
||||
gpi.pRasterizationState = &raster;
|
||||
gpi.pMultisampleState = &ms;
|
||||
@@ -576,13 +370,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
||||
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
|
||||
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
|
||||
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
|
||||
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
|
||||
// does not apply: while this is reachable it should keep saying so on every draw.
|
||||
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
|
||||
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
||||
VkResultToString(result),
|
||||
@@ -613,36 +400,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
||||
payload.vertexInputState->vertexBindingDescriptionCount,
|
||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
||||
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
||||
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
|
||||
// should-never-happen report and must survive in the INFO-level builds that CTS
|
||||
// actually runs against, alongside the MGLOG_F lines above.
|
||||
if (payload.stageSpirvDigests) {
|
||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
|
||||
"hash=0x%llx",
|
||||
i, digest.stage, digest.wordCount,
|
||||
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
|
||||
static_cast<unsigned long long>(digest.hash));
|
||||
}
|
||||
} else {
|
||||
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
|
||||
}
|
||||
if (payload.stages) {
|
||||
for (SizeT i = 0; i < payload.stages->size(); ++i) {
|
||||
const auto& stage = (*payload.stages)[i];
|
||||
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
|
||||
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
|
||||
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
|
||||
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
|
||||
"specialization=%d",
|
||||
i, static_cast<Uint32>(stage.stage),
|
||||
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
|
||||
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
|
||||
}
|
||||
}
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
const auto& attachment = payload.colorBlendAttachments[i];
|
||||
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
||||
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
|
||||
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
|
||||
// of any pipeline or program hash.
|
||||
struct ShaderStageSpirvDigest {
|
||||
Uint32 stage = 0; // VkShaderStageFlagBits
|
||||
Uint32 wordCount = 0;
|
||||
Uint64 hash = 0;
|
||||
};
|
||||
|
||||
class PipelineFactory {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
@@ -35,62 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType vertexInputHash = 0;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Nonzero is an exact, renderer-lifetime compatibility identity.
|
||||
// renderPass remains the valid creation handle, not the cache key.
|
||||
Uint64 wireRenderPassCompatibilityId = 0;
|
||||
#endif
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
|
||||
// identity and both are hashed. The renderer leaves the enable false unless the
|
||||
// device's sampleRateShading feature was enabled
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
|
||||
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
|
||||
// the levels glPatchParameterfv set are compiled INTO that module and are not a
|
||||
// function of the program or of patchControlPoints - see the note on
|
||||
// passthroughTessControlStage.
|
||||
Uint64 passthroughTessControlKey = 0;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
|
||||
// which vertex a flat varying takes AND the vertex order transform feedback records for
|
||||
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
|
||||
// Vulkan's own convention, which is what a device without the extension gets.
|
||||
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
Bool depthTestEnable = false;
|
||||
Bool depthWriteEnable = false;
|
||||
Bool depthBiasEnable = false;
|
||||
@@ -112,21 +54,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool fragmentReplacesDepth = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
// The tessellation control stage this renderer synthesized for a program that has
|
||||
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
|
||||
// fixed-function pass-through; Vulkan has no such thing and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
|
||||
// pipeline outright). Appended to `stages` at creation. A null module means the
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
|
||||
// of the default tessellation levels - the first two of which ComputeHash already
|
||||
// mixes in, and the third of which arrives through passthroughTessControlKey above.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||
};
|
||||
|
||||
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
||||
@@ -137,26 +65,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||
void DestroyAll();
|
||||
|
||||
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
// that stamp its factory entry). Returns the number destroyed.
|
||||
Uint32 EvictByProgramHash(HashType programHash);
|
||||
|
||||
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||
@@ -179,26 +87,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||
|
||||
private:
|
||||
struct PipelineCacheEntry {
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// The hashed inputs the eviction paths key on: programHash ties the entry to
|
||||
// its ProgramFactory entry, renderPass records the exact handle the hash
|
||||
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
|
||||
HashType programHash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
|
||||
// cache eviction (see OnFrameBoundary).
|
||||
Uint64 lastUsedFrame = 0;
|
||||
};
|
||||
|
||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
@@ -33,13 +32,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CombinedImageSampler,
|
||||
UniformTexelBuffer,
|
||||
StorageBuffer,
|
||||
StorageImage,
|
||||
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
|
||||
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
|
||||
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
|
||||
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
|
||||
// DescriptorKeyHash mixes the enumerator's value.
|
||||
StorageTexelBuffer
|
||||
StorageImage
|
||||
};
|
||||
|
||||
enum class CompileOptionBit : Uint {
|
||||
@@ -49,96 +42,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SurfaceRotate90 = 1 << 2,
|
||||
SurfaceRotate180 = 1 << 3,
|
||||
SurfaceRotate270 = 1 << 4,
|
||||
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
|
||||
// Only ever set for a draw whose every sampler binding is clamped to a single mip
|
||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
||||
ExplicitLod0Sampling = 1 << 5,
|
||||
// Decorates the last vertex-processing stage's captured varyings with
|
||||
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
|
||||
// recorded while GL transform feedback is active, so plain draws keep the
|
||||
// undecorated variant.
|
||||
XfbCapture = 1 << 6,
|
||||
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
|
||||
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
|
||||
// default framebuffer's image is stored in display (top-left) order, so a shader
|
||||
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
|
||||
// PositionYFlip (the two are the same fact about the same draws) except under a
|
||||
// quarter turn, which this renderer does not convert rectangles for either.
|
||||
FragCoordYFlip = 1 << 7,
|
||||
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
|
||||
// as zero for every drawing command that has no baseVertex parameter - all the
|
||||
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
|
||||
// for a non-indexed draw whose program actually reads the builtin, so nothing else
|
||||
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
// in the order glslang declares them - which is the order a redeclaration must use.
|
||||
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
|
||||
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
|
||||
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
|
||||
enum class PerVertexMemberBit : Uint32 {
|
||||
Position = 1u << 0,
|
||||
PointSize = 1u << 1,
|
||||
ClipDistance = 1u << 2,
|
||||
CullDistance = 1u << 3,
|
||||
};
|
||||
// What a program parsed below #version 450 carries, and the fallback when a module's
|
||||
// block cannot be read.
|
||||
static constexpr Uint32 kDefaultPerVertexMembers =
|
||||
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
|
||||
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
Uint32 maxPerStageUniformBuffers = 0;
|
||||
Uint32 maxPerStageStorageBuffers = 0;
|
||||
Uint32 maxPerStageSampledImages = 0;
|
||||
Uint32 maxPerStageStorageImages = 0;
|
||||
Uint32 maxPerStageResources = 0;
|
||||
Uint32 maxSetSamplers = 0;
|
||||
Uint32 maxSetUniformBuffers = 0;
|
||||
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||
Uint32 maxSetStorageBuffers = 0;
|
||||
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||
Uint32 maxSetSampledImages = 0;
|
||||
Uint32 maxSetStorageImages = 0;
|
||||
};
|
||||
|
||||
struct VkProgramObject {
|
||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||
|
||||
HashType hash = 0;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||
Vector<VkShaderModule> modules;
|
||||
// Parallel to stages; identifies the exact module bytes handed to the driver when a
|
||||
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
|
||||
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
|
||||
|
||||
// Layout data (previously in separate VkProgramLayout)
|
||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||
// True only when this layout passed every descriptor-indexing feature and
|
||||
// update-after-bind limit gate at reflection time. It controls both the
|
||||
// layout/binding flags and the pool class used by UniformManager.
|
||||
Bool usesUpdateAfterBind = false;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
Vector<DescriptorBindingKind> bindingKinds;
|
||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||
// 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was
|
||||
// scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes
|
||||
// pDynamicOffsets in binding order and the writer pushes them in iteration order, so an
|
||||
// unordered list would silently mis-pair dynamic offsets with their uniform blocks.
|
||||
Vector<Uint32> activeBindings;
|
||||
Vector<Uint32> dynamicBindings;
|
||||
Vector<Int> uniformBlockIndexByBinding;
|
||||
// Descriptor count per binding (1 except for a descriptor ARRAY - a UBO or storage
|
||||
// block instance array, an image uniform array or a sampler uniform array - each of
|
||||
// which occupies one binding with descriptorCount = N).
|
||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||
// binding with descriptorCount = N).
|
||||
Vector<Uint16> bindingDescriptorCounts;
|
||||
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
||||
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
||||
@@ -147,11 +69,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Int> samplerUniformLocationByBinding;
|
||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
|
||||
// the other, never both, and both need exactly the same thing - the format the
|
||||
// shader declared, so the per-draw resolve can tell a typed declaration from a
|
||||
// formatless one. Kept as one pair rather than two so the move operations below
|
||||
// cannot drift out of sync with a field that only one kind populates.
|
||||
Vector<VkFormat> storageImageFormatByBinding;
|
||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||
Vector<String> storageBlockNameByBinding;
|
||||
@@ -159,19 +76,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Set once during ReflectLayout so the per-draw path can skip the whole
|
||||
// storage-image preparation for the overwhelming majority of programs.
|
||||
Bool hasStorageImages = false;
|
||||
// Something about this program's descriptors could not be resolved - an opaque
|
||||
// uniform array whose elements have no addressable uniform locations (the
|
||||
// multi-dimensional case), or a binding remap that failed outright. The binding
|
||||
// STAYS DECLARED in the descriptor set layout; declining is done here, by refusing
|
||||
// every draw, and BindProgramUniformBuffers returns false so the draw setup skips
|
||||
// the draw exactly as it does for any other bind failure.
|
||||
//
|
||||
// Keeping the layout intact is the load-bearing half. Shrinking it instead - which
|
||||
// is what the first cut of this did - leaves the shader reading a descriptor the
|
||||
// layout never declared, and lavapipe segfaults on that inside PIPELINE CREATION,
|
||||
// in a JIT worker thread, before any draw runs where a refusal could help. The
|
||||
// reason was logged once at MGLOG_I when the descriptor was declined.
|
||||
Bool declinedDescriptors = false;
|
||||
Int globalUboBinding = -1;
|
||||
Uint32 activeVertexInputLocationMask = 0;
|
||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||
@@ -184,72 +88,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
|
||||
// program variant for non-indexed draws, and is deliberately a property of the
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
// The program has a tessellation or geometry module declaring TessellationPointSize /
|
||||
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
|
||||
// is off, so a pipeline built from it is invalid usage
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
|
||||
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
|
||||
// implements one level up, and the counterpart of the DirectGLES arm that reports a
|
||||
// driver with neither point-size extension by name.
|
||||
//
|
||||
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
// built-ins. A user-defined varying would arrive at the evaluation stage
|
||||
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
|
||||
// silently wrong pixels rather than a crash - so those programs are declined
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
|
||||
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
|
||||
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
|
||||
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
|
||||
// which the source processor rewrites to "#version 460 core" - carries four members
|
||||
// where a 430 program carries three. A fixed three-member pass-through fed the
|
||||
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
|
||||
// this whole family is written around.
|
||||
Uint32 passthroughPerVertexMembers = 0;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
mutable Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
|
||||
@@ -260,19 +98,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
modules = std::move(other.modules);
|
||||
// Must travel with `modules`: these digests name the SPIR-V those exact
|
||||
// shader modules were built from, and the pipeline-failure diagnostics
|
||||
// print the two together. Leaving it behind used to merely lose the
|
||||
// digests on a rehash; now that the cache is a robin-hood table, insertion
|
||||
// SWAPS two entries, and a field that no move touches stays behind in the
|
||||
// slot - pairing one program's modules with another program's digests, so
|
||||
// a pipeline failure would be reported against the wrong SPIR-V.
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
dynamicBindings = std::move(other.dynamicBindings);
|
||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||
@@ -287,7 +115,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -297,18 +124,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -316,12 +135,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
if (this == &other) {
|
||||
@@ -331,12 +144,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
modules = std::move(other.modules);
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
dynamicBindings = std::move(other.dynamicBindings);
|
||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||
@@ -351,7 +161,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -361,18 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -380,12 +181,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -412,108 +207,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
modules.clear();
|
||||
stages.clear();
|
||||
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||
}
|
||||
};
|
||||
|
||||
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
|
||||
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
|
||||
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
|
||||
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
|
||||
// layout handle value may be recycled for an unrelated layout, and the program
|
||||
// hash may be re-inserted by a later rebuild of the same content.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
||||
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
||||
// subgroup properties) so lowering can never disagree with the advertised
|
||||
// capabilities. Native subgroup operations always execute natively; the two
|
||||
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||
// on opted-in devices with no subgroup support at all.
|
||||
struct SubgroupLoweringPolicy {
|
||||
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
||||
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
||||
Uint32 nativeSubgroupSize = 0;
|
||||
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
||||
// workgroup needs more subgroups than this cannot request the flag.
|
||||
Uint32 maxComputeWorkgroupSubgroups = 0;
|
||||
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
||||
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
||||
Uint32 maxComputeSharedMemoryBytes = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool tessellationAndGeometryPointSizeEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||
// nothing that owns the device.
|
||||
~ProgramFactory();
|
||||
~ProgramFactory() = default;
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const;
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags);
|
||||
|
||||
// The default framebuffer's current image height, baked as a literal into every
|
||||
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
||||
// adding one for a value that changes only on swapchain recreation would cost the draw
|
||||
// path more than a recompile costs a resize). It is therefore part of those variants'
|
||||
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
|
||||
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
|
||||
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
|
||||
// otherwise keep using a module compiled against the old height.
|
||||
void SetDefaultFramebufferHeight(Uint32 height);
|
||||
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
|
||||
|
||||
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
||||
// an open-addressing map holding entries by value, so both moves existing entries.
|
||||
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||
// this is unchanged; on a bump it must re-run GetOrCreateProgram.
|
||||
Uint64 GetCacheStructureEpoch() const { return m_cacheStructureEpoch; }
|
||||
// A memoised entry pointer bypasses GetOrCreateProgram, whose per-lookup stamp is
|
||||
// what keeps an in-use entry out of OnFrameBoundary's idle sweep - so such a
|
||||
// caller must re-stamp the entry itself, at least once per frame boundary.
|
||||
void StampProgramUse(const VkProgramObject& entry) const { entry.lastUsedFrame = m_frameCounter; }
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep.
|
||||
void OnFrameBoundary();
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
|
||||
// sampler form above deliberately does not answer. Kept separate rather than folded in
|
||||
// because the two are asked in different places for different reasons: a sampler's domain
|
||||
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
|
||||
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
|
||||
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
|
||||
// format at all, and the numeric domain is then the only thing that constrains it.
|
||||
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
@@ -522,61 +236,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
|
||||
// draw with this program has to take the ZeroBaseVertex variant.
|
||||
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
|
||||
// the cache below is a rehashing map, so a pointer into it would not survive the next
|
||||
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
|
||||
// member set, because all three decide what the generator emits. The size comes from
|
||||
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
|
||||
// - draw state rather than link state, and the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
|
||||
// own SPIR-V, so two programs at different GLSL versions need different modules. The
|
||||
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
|
||||
// part of that pipeline's identity. Compiling is bounded by the number of distinct
|
||||
// (size, levels, members) combinations a program draws with - one or two in practice -
|
||||
// and only ever happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The identity of one such module: everything the generator bakes in, folded into a
|
||||
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
|
||||
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
|
||||
// same value into the pipeline hash so a pipeline can never be handed a module built for
|
||||
// different levels or a different block shape.
|
||||
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
|
||||
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
|
||||
// Input variable that is an array of a Block-decorated struct). Zero when the module has
|
||||
// no such block. Exposed for tests, which is the only way to pin the shape agreement
|
||||
// without a device.
|
||||
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -587,27 +246,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
||||
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
||||
// keeps the program's live attach list, which is a longer and differently-indexed list
|
||||
// the moment a glAttachShader lands after the link, from being passed here by mistake.
|
||||
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MagmaProgramSource& program, const Vector<Vector<Uint>>& spirv,
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
// records what the evaluation stage's input interface is made of.
|
||||
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
Uint32 m_maxBindings = 0;
|
||||
@@ -619,42 +265,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
|
||||
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
|
||||
// GeometryPointSize is refused at build time (see VkProgramObject::
|
||||
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||
// own counts would exceed the update-after-bind budget.
|
||||
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
SubgroupLoweringPolicy m_subgroupPolicy{};
|
||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||
Uint32 m_defaultFramebufferHeight = 0;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by the identity of what was compiled into
|
||||
// them - the input patch size and the six default tessellation levels, folded into one
|
||||
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
|
||||
// cannot simply be keyed on the patch size any more). A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
//
|
||||
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
|
||||
// nothing, so an application that recomputes a level per frame mints a new key per frame.
|
||||
// Reaching the cap destroys every module and starts over (see the flush in
|
||||
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
|
||||
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/SubmitFencePrefix.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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Collect every fence in the ordered submission prefix. Waiting on only the last matching
|
||||
// frame-serial record is insufficient: the same serial may have an earlier in-flight submit.
|
||||
template <typename Records, typename Fence>
|
||||
bool CollectSubmitFencePrefix(const Records& records, std::uint64_t submitIndex,
|
||||
Fence nullFence, std::vector<Fence>& fences) {
|
||||
fences.clear();
|
||||
bool foundTarget = false;
|
||||
for (const auto& record : records) {
|
||||
if (record.submitIndex > submitIndex) {
|
||||
break;
|
||||
}
|
||||
if (record.fence == nullFence) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
fences.push_back(record.fence);
|
||||
foundTarget = foundTarget || record.submitIndex == submitIndex;
|
||||
}
|
||||
if (!foundTarget) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -11,11 +11,6 @@
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<vulkan/vk_enum_string_helper.h>)
|
||||
#include <vulkan/vk_enum_string_helper.h>
|
||||
@@ -162,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
}
|
||||
|
||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||
@@ -171,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
@@ -252,11 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
|
||||
m_extent = createInfo.imageExtent;
|
||||
// The surface-space extent this swapchain was built from, i.e. before the
|
||||
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
|
||||
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
|
||||
// and makes the comparison alternate forever.
|
||||
m_surfaceExtent = defaultFramebufferExtent;
|
||||
m_preTransform = createInfo.preTransform;
|
||||
|
||||
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
|
||||
@@ -267,9 +257,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_images.resize(imageCount, VK_NULL_HANDLE);
|
||||
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
||||
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -285,6 +272,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<SizeT>(defaultFramebufferExtent.width) *
|
||||
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
|
||||
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
switch (m_depthStencilFormat) {
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
@@ -300,6 +292,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
break;
|
||||
}
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
// The default FBO's stencil attachment must track the swapchain extent:
|
||||
// FramebufferObject::CheckCompleteness requires every valid attachment
|
||||
@@ -319,44 +316,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilFormat = depthFormat;
|
||||
break;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
|
||||
// attachments are CLIENT memory and this thread may not write them - the backend
|
||||
// fills MGPSurfaceInfo and posts, and the client's consumer replays exactly the
|
||||
// allocate/format writes of the monolith arm below on the GL thread. The two
|
||||
// formats are always equal by the switches above, so one InternalFormat carries
|
||||
// both. The callback's presence IS the transport probe - the server session
|
||||
// installs it at Accept, and under monolith nobody ever does.
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = defaultFramebufferExtent.width;
|
||||
info.Height = defaultFramebufferExtent.height;
|
||||
info.InternalFormat = static_cast<Uint32>(depthFormat);
|
||||
info.Samples = 1;
|
||||
info.Layers = 1;
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
}
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
}
|
||||
|
||||
@@ -464,39 +428,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_images.clear();
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
return m_imageContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
m_imageContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
return m_depthStencilContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
m_depthStencilContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
void SwapchainObject::SetAllDepthStencilContentUndefined() {
|
||||
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
|
||||
m_depthStencilContentDefined[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
VkImage SwapchainObject::GetImage(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
||||
return m_images[index];
|
||||
|
||||
@@ -35,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
// created from - the value to compare a freshly queried currentExtent against.
|
||||
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
|
||||
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
|
||||
const Vector<VkImage>& GetImages() const { return m_images; }
|
||||
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
|
||||
@@ -52,21 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
||||
SizeT GetImageCount() const { return m_images.size(); }
|
||||
|
||||
// EGL content-validity tracking for the default framebuffer. A color
|
||||
// buffer's content is undefined once its image has been presented
|
||||
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
|
||||
// and every ancillary (depth/stencil) buffer's content is undefined
|
||||
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
|
||||
// render-pass manager turns an undefined attachment's tile load into
|
||||
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
|
||||
// garbage) and a render pass storing into an attachment sets it back
|
||||
// to defined.
|
||||
Bool IsImageContentDefined(Uint32 index) const;
|
||||
void SetImageContentDefined(Uint32 index, Bool defined);
|
||||
Bool IsDepthStencilContentDefined(Uint32 index) const;
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -81,7 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
Vector<VkImage> m_images;
|
||||
Vector<VkImageView> m_imageViews;
|
||||
@@ -92,7 +73,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
||||
Vector<VkImageView> m_depthStencilImageViews;
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "VkSamplerManager.h"
|
||||
#include "VkTextureManager.h"
|
||||
#include "WirePlaceholderKey.h"
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
|
||||
@@ -28,26 +26,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
struct SamplerBindingOverride {
|
||||
Uint32 binding = 0;
|
||||
Uint32 element = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Bool forceNearestFiltering = false;
|
||||
};
|
||||
|
||||
struct SamplerImageFeedbackBinding {
|
||||
Uint32 samplerBinding = 0;
|
||||
Uint32 samplerElement = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
|
||||
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
|
||||
// texel buffer, and there is no other route to that answer from here.
|
||||
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||
@@ -55,92 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A present-less wire frame may run arbitrarily many draws. Before its
|
||||
// descriptor cursor reaches this budget, the renderer retires the
|
||||
// submission that last used the sets and rewinds every layout cursor.
|
||||
static constexpr Uint32 kWireDescriptorSetBudget = 2048;
|
||||
Bool WireDescriptorSetBudgetReached(Uint32 frameIndex) const;
|
||||
SizeT RewindWireDescriptorSets(Uint32 frameIndex);
|
||||
// How the wire arm answers a STORAGE image unit that GL 4.6 core 8.26 makes invalid (no
|
||||
// texture, or a (level, layer) its texture lacks): a load returns zero, a store and an
|
||||
// atomic are discarded. `deviceNullDescriptor` is the renderer's enablement of
|
||||
// VK_EXT_robustness2's nullDescriptor at device creation. With it the unit binds a NULL
|
||||
// storage-image descriptor, which is exactly that rule. Without it (or under
|
||||
// MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER=1, the host lanes' way onto this arm) the
|
||||
// unit binds a placeholder PRIVATE to its unit (per shape), cleared before each use, so no
|
||||
// two units ever alias; what that cannot give is a load of the SAME unit after its own
|
||||
// store inside one pass reading zero, nor privacy past kWirePrivateStoragePlaceholderCap
|
||||
// placeholders (CONTRACT-P7 §12). Called after Initialize.
|
||||
void SetWireInvalidStorageImageArm(Bool deviceNullDescriptor);
|
||||
#endif
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
// vkFreeDescriptorSets'd back to their pools (created with
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
|
||||
// in binding order, and within a binding in array-element order): the resolved texture
|
||||
// and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
|
||||
// at the same heap address can only MISS a comparison, never false-hit it (same ABA
|
||||
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
|
||||
// element granularity is required, or swapping the textures of two elements of the same
|
||||
// array would leave the record list identical and the fast path would keep a stale set.
|
||||
struct SampledBindingRecord {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MagmaProgramSource& program,
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||
// walk and reports whether every visited binding still resolves to the recorded
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve lazy texture uploads/promotions before the caller captures the
|
||||
// command buffer passed to BindProgramUniformBuffers. Preparation may
|
||||
// submit older work, but descriptor recording must never rotate it.
|
||||
Bool PrepareWireTextureResources(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
#endif
|
||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||
// every input of every combined-image-sampler resolution is unchanged since the
|
||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||
// sum, sampling-resolution generation (sampler params + texture shape), image
|
||||
// epochs AND per-resource layout values - so the per-binding cached
|
||||
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||
Bool samplerDescriptorsUnchangedHint = false,
|
||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
@@ -148,219 +58,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat);
|
||||
|
||||
// True when the program reads at least one sampler and every one of them is bound to a
|
||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
private:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
// owning bucket's accounting.
|
||||
struct CachedDescriptorSet {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct DescriptorSetCacheEntry {
|
||||
Vector<CachedDescriptorSet> sets;
|
||||
Vector<VkDescriptorSet> sets;
|
||||
Uint32 cursor = 0;
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ResolveWireImageDescriptor emits no pNext chain. Key every view-create
|
||||
// value explicitly: hash collisions must never alias different windows,
|
||||
// formats or swizzles. The root handle and allocation epoch also prevent
|
||||
// a recycled native image handle from reviving an older view.
|
||||
struct WireImageViewKey {
|
||||
MG_Pipe::MGPipeHandle root{};
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
Uint64 imageEpoch = 0;
|
||||
VkImageViewCreateFlags flags = 0;
|
||||
VkImageViewType type = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkComponentMapping components{};
|
||||
VkImageSubresourceRange range{};
|
||||
|
||||
Bool operator==(const WireImageViewKey& other) const {
|
||||
return root == other.root && image == other.image && imageEpoch == other.imageEpoch &&
|
||||
flags == other.flags && type == other.type && format == other.format &&
|
||||
components.r == other.components.r && components.g == other.components.g &&
|
||||
components.b == other.components.b && components.a == other.components.a &&
|
||||
range.aspectMask == other.range.aspectMask && range.baseMipLevel == other.range.baseMipLevel &&
|
||||
range.levelCount == other.range.levelCount && range.baseArrayLayer == other.range.baseArrayLayer &&
|
||||
range.layerCount == other.range.layerCount;
|
||||
}
|
||||
};
|
||||
|
||||
struct WireImageViewKeyHash {
|
||||
SizeT operator()(const WireImageViewKey& key) const;
|
||||
};
|
||||
|
||||
struct WirePlaceholderImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
VkRenderPass clearPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer clearFramebuffer = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Uint32 layers = 1;
|
||||
};
|
||||
// Keyed by shape, and a storage placeholder on the arm without a null descriptor by its
|
||||
// image unit too (WirePlaceholderKey.h: ChooseWirePlaceholderKey, and its cap).
|
||||
mutable UnorderedMap<WirePlaceholderKey, WirePlaceholderImage, WirePlaceholderKeyHash> m_wirePlaceholderImages;
|
||||
// How many of those are private to a unit (against kWirePrivateStoragePlaceholderCap).
|
||||
mutable SizeT m_wirePrivateStoragePlaceholders = 0;
|
||||
// See SetWireInvalidStorageImageArm.
|
||||
Bool m_wireInvalidStorageImagesBindNull = false;
|
||||
// boundStorageFormat: for a storage binding with no reflected format, the format the
|
||||
// unit's glBindImageTexture named, when the unit holds a texture it cannot address
|
||||
// (GL 4.6 core 8.26); UNDEFINED keeps the numeric-domain R32 default. `unit` is the image
|
||||
// unit of a storage binding (what its private placeholder is private to); unused for a sampler.
|
||||
Bool ResolveWirePlaceholderImage(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
|
||||
VkDescriptorImageInfo& out, VkFormat boundStorageFormat = VK_FORMAT_UNDEFINED, Uint32 unit = ~0u) const;
|
||||
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
|
||||
#endif
|
||||
|
||||
struct FrameResources {
|
||||
Vector<DescriptorPoolBucket> descriptorPools;
|
||||
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
|
||||
Vector<VkBufferView> texelBufferViews;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
mutable Vector<VkImageView> wireImageViews;
|
||||
// Own views in the vector above until this slot's fence/idle proof.
|
||||
// Cache hits preserve the handle, enabling descriptor-content reuse.
|
||||
mutable UnorderedMap<WireImageViewKey, VkImageView, WireImageViewKeyHash> wireImageViewCache;
|
||||
#endif
|
||||
Uint32 activeDescriptorPoolIndex = 0;
|
||||
Uint32 allocatedSetsThisFrame = 0;
|
||||
Uint32 peakAllocatedSetsThisFrame = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||
// texture after the fallback substitution (may still be null when no fallback
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||
Bool ResolveSampledBinding(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
|
||||
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
|
||||
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
|
||||
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
|
||||
// one shared object, because a descriptor whose format disagrees with the shader's
|
||||
// declaration is invalid Vulkan even when nothing ever reads it.
|
||||
//
|
||||
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
|
||||
// texel buffer, which never carries one, or for a formatless `writeonly` image);
|
||||
// `numericDomain` decides the format when there is no declaration and is the fallback
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
//
|
||||
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||
// obligations the caller carries). The cache is keyed by binding alone, so it is
|
||||
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint = false) const;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
|
||||
Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, Bool storage, VkBufferView& out);
|
||||
Uint32 m_wireFrameIndex = 0;
|
||||
VkDeviceSize m_wireStorageOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireTexelOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0};
|
||||
VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0};
|
||||
Uint32 m_wireMaxTexelElements = ~Uint32{0};
|
||||
#endif
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
|
||||
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
|
||||
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
|
||||
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
|
||||
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
|
||||
Bool ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||
// binding point, buffer and glBindBufferRange window.
|
||||
Bool ResolveStorageBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
// `element` indexes an image ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL image unit.
|
||||
VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
|
||||
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
|
||||
struct UboBindResult {
|
||||
@@ -371,30 +114,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* payload = nullptr; // fallback UploadTransient path
|
||||
VkDeviceSize payloadSize = 0;
|
||||
};
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex,
|
||||
Uint32 bindingPoint, UboBindResult& out) const;
|
||||
#endif
|
||||
Bool ResolveUniformBufferPayload(const MagmaProgramSource& program,
|
||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, UboBindResult& out) const;
|
||||
// Shared resolution of one dynamic-UBO binding element into the
|
||||
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
|
||||
// bind, global-slice reuse, or transient upload. Used by the full walk
|
||||
// and by the dynamic-offset-only rebind (see FastRebindMemo).
|
||||
Bool ResolveDynamicUboDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
|
||||
VkDeviceSize& outRange, Uint32& outDynamicOffset);
|
||||
// The vkCmdBindDescriptorSets tail shared by the full walk and the
|
||||
// dynamic-offset-only rebind: skips the driver call when this exact
|
||||
// binding is already live on the command buffer (see the bind-dedup
|
||||
// shadow below), otherwise binds and refreshes the shadow.
|
||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
@@ -402,7 +126,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet);
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VkBufferManager* m_bufferManager = nullptr;
|
||||
ProgramFactory* m_programFactory = nullptr;
|
||||
Vector<FrameResources> m_frames;
|
||||
@@ -415,18 +138,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
@@ -436,94 +147,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkBufferView> m_texelBufferViewsScratch;
|
||||
Vector<Uint32> m_dynamicOffsetsScratch;
|
||||
|
||||
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
|
||||
// When a draw's resolved descriptor content is byte-identical to one memoized
|
||||
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
|
||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
|
||||
// entries with round-robin replacement rather than one: draws alternating
|
||||
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
|
||||
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
|
||||
// because the frame's descriptor sets are recycled there.
|
||||
struct DescriptorReuseEntry {
|
||||
Uint64 signature = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
Bool valid = false;
|
||||
};
|
||||
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
|
||||
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||
Uint32 m_descriptorReuseMemoNext = 0;
|
||||
|
||||
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
|
||||
// descriptor set selected by the last cacheable full walk of a program
|
||||
// whose active bindings are exactly one dynamic UBO (single descriptor)
|
||||
// plus combined-image samplers. When the next call proves every sampler
|
||||
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
|
||||
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
|
||||
// moved, the per-draw glUniform case - the walk collapses to: resolve one
|
||||
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
|
||||
// rebinding the same set with different dynamic offsets).
|
||||
// Invalidation inventory: BeginFrame clears it (the frame's sets are
|
||||
// recycled) and the frameIndex field guards cross-frame confusion on top;
|
||||
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
|
||||
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
|
||||
// program relink bumps the backend state version and thus programObj.hash
|
||||
// so the key misses; the program lifetime id is never reused, so a
|
||||
// deleted-and-recreated program misses; a texture/sampler/binding change
|
||||
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||
// advances, so the recorded set is never re-written within its frame.
|
||||
// P7 M2 round 2 (ID-P7-43): a WIRE store can die MID-FRAME on the reclaim
|
||||
// path (VkBufferManager::DeferredWireRelease), and the next mint can hand
|
||||
// its VkBuffer handle value back - a heap pointer under lavapipe - so "the
|
||||
// same VkBuffer+range" is no longer proof of the same store. The memo
|
||||
// therefore records VkBufferManager::GetWireStoreDestroyEpoch() and misses
|
||||
// once any wire store has been destroyed since; the descriptor-reuse
|
||||
// signature below folds the same epoch in. On the wire arm this memo is
|
||||
// never consulted (SetupWireDraw passes no sampler hint), so the field is
|
||||
// the defensive half; the signature is the half the wire arm reaches.
|
||||
struct FastRebindMemo {
|
||||
Bool valid = false;
|
||||
Uint32 frameIndex = 0;
|
||||
Uint64 programLifetimeId = 0;
|
||||
ProgramFactory::HashType programHash = 0;
|
||||
Uint32 uboBinding = 0;
|
||||
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize uboRange = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
};
|
||||
FastRebindMemo m_fastRebindMemo;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
|
||||
// layout+bind point, so a pipeline-layout switch always rebinds.
|
||||
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
|
||||
Bool m_lastBindValid = false;
|
||||
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
|
||||
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
Uint32 m_lastBindOffsetCount = 0;
|
||||
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
|
||||
|
||||
// Global-UBO transient-slice reuse: MC leaves the default uniform block
|
||||
// untouched across long GUI/terrain runs, so the per-draw re-upload of
|
||||
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
|
||||
// serial guards arena recycling; the content version guards writes).
|
||||
struct GlobalUboSliceMemo {
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint64 frameSerial = 0;
|
||||
Uint32 uboContentVersion = 0;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
VkDeviceSize range = 0;
|
||||
};
|
||||
static constexpr Uint32 kGlobalUboMemoSize = 4;
|
||||
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
|
||||
Uint32 m_globalUboMemoNext = 0;
|
||||
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
|
||||
// When a draw's resolved descriptor content is byte-identical to the previous
|
||||
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
|
||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
|
||||
// frame in BeginFrame because the frame's descriptor sets are recycled there.
|
||||
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
|
||||
Uint64 m_lastDescriptorSignature = 0;
|
||||
Bool m_hasLastDescriptor = false;
|
||||
|
||||
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
|
||||
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
|
||||
@@ -536,15 +167,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
Uint32 viewLevelCount = 0;
|
||||
Uint16 samplerVersion = 0;
|
||||
Uint16 textureParamsVersion = 0;
|
||||
Bool forceNearestFiltering = false;
|
||||
@@ -556,36 +183,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
Bool viewFormatValid = false;
|
||||
// Whole resolved descriptor from this binding's last full resolve. Reused
|
||||
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
|
||||
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
||||
//
|
||||
// This one field is keyed by binding but describes ONE descriptor, so it is
|
||||
// written and read only for single-descriptor bindings. A sampler ARRAY's
|
||||
// elements share the binding and would overwrite each other here - the last
|
||||
// element resolved would then be handed to element 0 on the next hinted draw.
|
||||
// Every other field above is self-validating (each compares its full key
|
||||
// before reuse, and the view-format entry is a pure function of format and
|
||||
// numeric domain), so an arrayed binding may keep using those.
|
||||
VkDescriptorImageInfo info{};
|
||||
Bool infoValid = false;
|
||||
};
|
||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
|
||||
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
|
||||
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
|
||||
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
|
||||
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
|
||||
// an entry's *Valid flags on raises this mark first, so entries at or above it are
|
||||
// provably still in their constructed (all-invalid) state and clearing them is a
|
||||
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
|
||||
mutable Uint32 m_samplerResolveMemoHighWater = 0;
|
||||
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
|
||||
if (binding >= m_samplerResolveMemoHighWater) {
|
||||
m_samplerResolveMemoHighWater = binding + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -7,13 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -35,192 +29,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||
// hash. An address is recycled by the allocator, so a deleted-and-recreated
|
||||
// buffer reproduces it; combined with a byte-identical attribute layout that
|
||||
// reproduces the WHOLE content hash, and the hash is what
|
||||
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
|
||||
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
|
||||
// slice was bound for its successor's draw, which is how a transform-feedback
|
||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
//
|
||||
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
|
||||
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
|
||||
// into every server-side content hash". The two are equally ABA-proof (the
|
||||
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
|
||||
// changes is that the key is now the identity the SERVER will be handed once
|
||||
// buffers travel as handles, instead of a number only the client can mint.
|
||||
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (attr.Buffer) {
|
||||
// The SAME arm question the other four re-keyed sites ask, through the same
|
||||
// helper: a site that decided for itself could silently key on the pre-handle
|
||||
// identity while its neighbours keyed on the handle.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||
}
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
|
||||
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
|
||||
// wearing two hats, and a control that defeated only the retired one would
|
||||
// say nothing about the key P2 ships.
|
||||
//
|
||||
// The identity is replaced by a constant rather than by the raw
|
||||
// BufferObject*, because the address is not recycled in practice and so
|
||||
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
|
||||
// key with NO buffer identity in it looks like - the exact defect this
|
||||
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
|
||||
// accepts as proof that a memoised binding still reads the buffer it was
|
||||
// resolved from", and with the identity gone it accepts a binding resolved
|
||||
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
|
||||
// replacement VAO and gets its dead predecessor's vertex data.
|
||||
bufferKey = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||
//
|
||||
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Uint64& outHash) const {
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||
outHash = memos.Hash;
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
HashType hash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||
return memos.Hash;
|
||||
}
|
||||
hash = ComputeHash(vao);
|
||||
memos.Hash = hash;
|
||||
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||
return hash;
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
if (!vao.GetBackendHashMemo(hash)) {
|
||||
hash = ComputeHash(vao);
|
||||
vao.SetBackendHashMemo(hash);
|
||||
}
|
||||
#endif
|
||||
return hash;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||
// memo does not change that.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||
memos.StateEpoch == m_evictionEpoch) {
|
||||
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *memoEntry;
|
||||
}
|
||||
const BackendVertexInputState& resolved =
|
||||
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||
// the reference is not worth holding across a call that can resize the table.
|
||||
VaoBackendMemos& stamp = MemosFor(vao);
|
||||
stamp.State = &resolved;
|
||||
stamp.StateEpoch = m_evictionEpoch;
|
||||
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||
return resolved;
|
||||
}
|
||||
#endif
|
||||
#if !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||
// time true, so the handle arm above always returns. Written out rather than left to
|
||||
// fall off the end so the function still has a return on every path a compiler sees.
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
#else
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
const void* memoState = nullptr;
|
||||
Uint64 memoEpoch = 0;
|
||||
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
|
||||
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
|
||||
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *entry;
|
||||
}
|
||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
|
||||
// memo (pure VALUES derived from the VAO configuration, so config-version
|
||||
// guarding alone is sound). The draw fast path reads them from the VAO object it
|
||||
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
|
||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||
return entry;
|
||||
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
@@ -229,7 +67,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
@@ -238,46 +75,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// module, so a float32 stream would be fed to a Float64 input.
|
||||
const Bool narrowFloat64Arrays =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
|
||||
// SERVER's own backend's - the client caps mirror is client memory (rule E).
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
||||
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
||||
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
||||
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
||||
// than dropped. Mirrors what DirectGLES does for the same state.
|
||||
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
||||
sourceVkFormat = narrowedFormat;
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
||||
"float32 precision through format=%d (size=%d long=%s)",
|
||||
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
||||
}
|
||||
}
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -285,13 +86,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkFormat vkFormat = sourceVkFormat;
|
||||
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
if (!SupportsVertexBufferFormat(vkFormat)) {
|
||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
@@ -301,7 +103,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -312,21 +114,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
|
||||
// so a zero here is the binding model's stride 0 - every vertex reads the same
|
||||
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
|
||||
// means. Substituting the element size fetched a fresh element per vertex and ran
|
||||
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
|
||||
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
|
||||
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
|
||||
const Uint32 sourceStride =
|
||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
@@ -341,23 +137,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
// A converted stream is tightly packed, so its stride is the converted element
|
||||
// size - unless the source stride is zero, which does not describe a packing at
|
||||
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||
// converts exactly one element and every vertex reads it.
|
||||
if (sourceStride != 0) {
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
||||
conversion == VertexStreamConversion::Float64ToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
@@ -371,51 +160,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
|
||||
// else needs the extension to say it.
|
||||
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
|
||||
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
|
||||
}
|
||||
}
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
auto& entry = m_cache[hash];
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindingDivisors = Move(bindingDivisors);
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
for (const auto& divisor : entry.bindingDivisors) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
@@ -425,193 +177,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
||||
if (!entry.bindingDivisors.empty()) {
|
||||
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
|
||||
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
|
||||
entry.state.pNext = &entry.divisorState;
|
||||
} else {
|
||||
entry.state.pNext = nullptr;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
|
||||
out = BackendVertexInputState{};
|
||||
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
|
||||
const auto& attr = elements.Attributes[location];
|
||||
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-layout`: THE ONE STRING BECAME TWO
|
||||
// VERDICTS, and the split is by what the reason is ABOUT rather than by severity.
|
||||
//
|
||||
// Every reason here used to return false, and the single caller answered that with
|
||||
// one P7-marked MagmaWireFatal - an abort, bypassing Session::Fail, invisible
|
||||
// to the census gate and with no equivalent on the monolith arm at all. But the
|
||||
// reasons are not one kind of thing:
|
||||
//
|
||||
// * `buffer-window` is a statement about the RECORD: an enabled attribute the
|
||||
// program reads names a slot outside the window set_vertex_buffers published.
|
||||
// Nothing about the device or the format is involved; the two halves of the
|
||||
// protocol disagree about what crossed. It stays a named Fatal through the hook
|
||||
// (see the caller), and it is load-bearing rather than defensive - it is what
|
||||
// caught the Redmi VAO hash collision fixed in 376c04be.
|
||||
//
|
||||
// * every other reason is a statement about what VULKAN CAN EXPRESS on this
|
||||
// device: a GL type with no VkFormat, a format without
|
||||
// VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT, a component size we cannot compute, an
|
||||
// offset sum that does not fit. The monolith arm has had an answer for these
|
||||
// since it was written, and it is not an abort: mask the attribute out of the
|
||||
// vertex input state and carry on (the `unsupportedAttribMask |= ...; continue;`
|
||||
// sites in GetOrCreateVertexInputState above). The draw path then decides,
|
||||
// loudly and once, whether the masked attribute was one the program actually
|
||||
// reads. This arm now does exactly that, which is what "same observable as the
|
||||
// monolith lane" means for this row.
|
||||
//
|
||||
// THE COUNT: the contract says "the other six"; there are SEVEN return sites below
|
||||
// it, because `offset-overflow` was added after the audit's count and is an
|
||||
// arithmetic guard rather than a shape one. It is masked with the rest: an
|
||||
// attribute whose base offsets cannot be added is one Vulkan cannot fetch, the
|
||||
// monolith arm never computes that sum at all, and masking is strictly safer than
|
||||
// a Fatal for a number no application can reach on purpose. Recorded in
|
||||
// notes/p7/magma-c.md rather than silently reconciled.
|
||||
const auto describe = [&](const char* reason, VkFormat format) {
|
||||
MGLOG_E_ONCE("Magma wire vertex layout: %s location=%u type=%u size=%u normalized=%u integer=%u long=%u bgra=%u stride=%d offset=%llu format=%d bufferWindow=%u+%u activeMask=0x%x",
|
||||
reason, location, attr.Type, static_cast<Uint32>(attr.Size), static_cast<Uint32>(attr.Normalized),
|
||||
static_cast<Uint32>(attr.IsInteger), static_cast<Uint32>(attr.IsLong), static_cast<Uint32>(attr.IsBgra),
|
||||
attr.Stride, static_cast<unsigned long long>(attr.Offset), static_cast<Int>(format),
|
||||
state.VertexBufferStart, state.VertexBufferCount, activeMask);
|
||||
};
|
||||
// The protocol verdict: the caller turns a false into the named Fatal.
|
||||
const auto reject = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
return false;
|
||||
};
|
||||
// The device verdict: MGLOG_E_ONCE + mask + continue, the monolith arm's shape.
|
||||
// MGLOG_E_ONCE and not MGLOG_E, also the monolith arm's: an unmappable attribute is
|
||||
// a property of the VAO and the device, so it repeats every draw, and the per-draw
|
||||
// line is what made the old abort look preferable to whoever wrote it.
|
||||
const auto maskOut = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
out.unsupportedAttribMask |= 1u << location;
|
||||
};
|
||||
// set_vertex_buffers is flattened PER ATTRIBUTE (VertexInputEmit.h), not
|
||||
// indexed by the original ARB binding point in attr.BindingIndex.
|
||||
if (location < state.VertexBufferStart ||
|
||||
location - state.VertexBufferStart >= state.VertexBufferCount) return reject("buffer-window");
|
||||
const auto& buffer = state.VertexBuffers[location];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
if (attr.Stride < 0 || attr.Size < 1 || attr.Size > 4) { maskOut("attribute-shape"); continue; }
|
||||
VkFormat format = ToVkVertexFormat(type, attr.Size, attr.Normalized, attr.IsInteger,
|
||||
attr.IsBgra, attr.IsLong);
|
||||
auto conversion = VertexStreamConversion::None;
|
||||
if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) {
|
||||
const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
if (backend && backend->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
// Exactly the monolith arm's answer for the same state: with native fp64
|
||||
// the module KEPT its 64-bit inputs (DemoteFloat64Pass did not run), so
|
||||
// narrowing the stream would feed float32 to a Float64 input - and the
|
||||
// monolith build therefore leaves sourceVkFormat UNDEFINED and falls into
|
||||
// its own mask-out below. Same place, same mask.
|
||||
maskOut("native-fp64-format");
|
||||
continue;
|
||||
}
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED) { maskOut("format-map"); continue; }
|
||||
if (!SupportsVertexBufferFormat(format)) {
|
||||
if (!IsScaledIntegerVertexFormat(format)) { maskOut("native-format-feature", format); continue; }
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
if (!SupportsVertexBufferFormat(format)) { maskOut("converted-format-feature", format); continue; }
|
||||
}
|
||||
const SizeT elementSize = GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (!elementSize) { maskOut("element-size", format); continue; }
|
||||
const Uint64 offset = attr.Offset + buffer.Offset;
|
||||
if (offset < attr.Offset) { maskOut("offset-overflow", format); continue; }
|
||||
const SizeT alignment = (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev)
|
||||
? elementSize : GetComponentSize(type);
|
||||
if (conversion == VertexStreamConversion::None && alignment > 1 &&
|
||||
(offset % alignment || static_cast<Uint32>(attr.Stride) % alignment))
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
Uint32 stride = static_cast<Uint32>(attr.Stride);
|
||||
if (stride && conversion != VertexStreamConversion::None)
|
||||
stride = conversion == VertexStreamConversion::Repack ? static_cast<Uint32>(elementSize)
|
||||
: static_cast<Uint32>(attr.Size) * sizeof(Float);
|
||||
const Uint32 binding = static_cast<Uint32>(out.bindings.size());
|
||||
out.bindings.push_back({binding, stride,
|
||||
buffer.Divisor ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
out.attributes.push_back({location, binding, format, 0});
|
||||
out.bindingAttributeLocations.push_back(location);
|
||||
out.bindingBaseOffsets.push_back(static_cast<SizeT>(offset));
|
||||
out.bindingConversions.push_back(conversion);
|
||||
if (buffer.Divisor > 1) out.bindingDivisors.push_back({binding, buffer.Divisor});
|
||||
out.attributeLocationMask |= 1u << location;
|
||||
}
|
||||
// Native handles/offsets do not decide the pipeline layout. Include only the
|
||||
// resolved binding/attribute/divisor values, including a legal zero stride.
|
||||
Uint64 hash = XXH64(out.bindings.data(), out.bindings.size() * sizeof(out.bindings[0]), 0);
|
||||
hash = XXH64(out.attributes.data(), out.attributes.size() * sizeof(out.attributes[0]), hash);
|
||||
hash = XXH64(out.bindingDivisors.data(),
|
||||
out.bindingDivisors.size() * sizeof(out.bindingDivisors[0]), hash);
|
||||
// THE MASK IS PART OF THE LAYOUT NOW, for the same reason the monolith entry's hash
|
||||
// carries it (the XXH64_update over unsupportedAttribMask in GetOrCreateVertexInputState
|
||||
// above). Before this package the mask was always zero here, so leaving it out of the
|
||||
// hash was free; now two VAOs can produce the SAME bindings, attributes and divisors
|
||||
// and differ only in which enabled attribute was masked out, and a hash blind to that
|
||||
// would serve one of them the other's pipeline.
|
||||
out.layoutHash = XXH64(&out.unsupportedAttribMask, sizeof(out.unsupportedAttribMask), hash);
|
||||
out.state.vertexBindingDescriptionCount = static_cast<Uint32>(out.bindings.size());
|
||||
out.state.pVertexBindingDescriptions = out.bindings.data();
|
||||
out.state.vertexAttributeDescriptionCount = static_cast<Uint32>(out.attributes.size());
|
||||
out.state.pVertexAttributeDescriptions = out.attributes.data();
|
||||
if (!out.bindingDivisors.empty()) {
|
||||
out.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(out.bindingDivisors.size());
|
||||
out.divisorState.pVertexBindingDivisors = out.bindingDivisors.data();
|
||||
out.state.pNext = &out.divisorState;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||
// reference into the current entry across its setup, and unordered_map
|
||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||
// from the VAO state (same hash, same content).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment). With no legacy arm the memos
|
||||
// live in this factory and die with it, so a per-instance bump is
|
||||
// enough - P2 D12.5.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
++m_evictionEpoch;
|
||||
#endif
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra, Bool isLong) {
|
||||
Bool isBgra) {
|
||||
if (isBgra) {
|
||||
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
||||
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
||||
@@ -636,46 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case DataType::Int2101010Rev:
|
||||
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
||||
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
||||
case DataType::Float64:
|
||||
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
|
||||
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
|
||||
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
|
||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||
// so they always agree without extra plumbing.
|
||||
//
|
||||
// ... as long as the shader half still runs. It does not when the backend has declared
|
||||
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
|
||||
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
|
||||
// and a UINT-formatted attribute would be fed to a float input - garbage with no
|
||||
// diagnostic anywhere. Declining here hands the attribute to the caller's
|
||||
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
|
||||
// demoted `vec` input - the same thing DirectGLES does for the same state. The
|
||||
// frontend RECORDS the format either way, so this gate is the only thing standing
|
||||
// between a legal glVertexAttribLFormat and a mismatched pipeline.
|
||||
if (
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): see the narrowFloat64Arrays site above.
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||
switch (size) {
|
||||
case 1: return VK_FORMAT_R32G32_UINT;
|
||||
case 2: return VK_FORMAT_R32G32B32A32_UINT;
|
||||
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
|
||||
// over two attribute locations, which the location-per-VAO-index model here does not
|
||||
// express. Declined rather than fetched wrong.
|
||||
default: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
case DataType::Float32:
|
||||
switch (size) {
|
||||
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||
@@ -847,17 +377,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
}
|
||||
#endif
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
|
||||
@@ -7,15 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
||||
// so the pull build gains nothing from it.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
#include "Config.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
@@ -30,26 +23,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
None = 0,
|
||||
Repack,
|
||||
ScaledIntegerToFloat32,
|
||||
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
|
||||
// of the fp64 demotion the shader side already does unconditionally.
|
||||
Float64ToFloat32,
|
||||
};
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||
// unsupported mask) - NO buffer identities. `hash` mixes each bound
|
||||
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
|
||||
// identity per buffer; keying pipelines on that minted one VkPipeline per
|
||||
// chunk section for an identical layout, defeating pipeline reuse and the
|
||||
// per-draw memo. Pipelines depend only on the layout, so they key on this
|
||||
// instead.
|
||||
HashType layoutHash = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
||||
// a const entry reference.
|
||||
mutable Uint64 lastUsedFrameBoundary = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
Vector<VkVertexInputAttributeDescription> attributes;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
@@ -61,167 +38,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
||||
// rate advances once per instance and nothing else, so anything else has to be
|
||||
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
||||
// binding uses divisor 1, which is what the plain input rate already means.
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
|
||||
};
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
};
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||
MagmaPipeIdentityTables& identity):
|
||||
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||
#else
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
#endif
|
||||
~VertexInputStateFactory() = default;
|
||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||
|
||||
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
||||
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
||||
// Readers that find the aux memo valid can use these without resolving the entry.
|
||||
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
||||
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
||||
}
|
||||
|
||||
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||
// is unchanged. Use this on per-draw paths.
|
||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||
// question ask the factory instead, because that is where the memo lives once the
|
||||
// frontend object stops carrying the backend's state.
|
||||
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
||||
#endif
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
||||
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
||||
// not by luck: a recreated buffer can no longer land back on its dead
|
||||
// predecessor's key. Without eviction the map grows for the whole session.
|
||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||
// and the draw path's entry reference never spans a frame boundary, so
|
||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||
// compare except on sweep boundaries.
|
||||
void OnFrameBoundary();
|
||||
static SizeT GetComponentSize(DataType type);
|
||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
// an unknown/unsupported type.
|
||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A wire VAO is a layout record plus a separate per-attribute buffer window.
|
||||
// No frontend VAO identity, address or memo participates in this layout.
|
||||
Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
|
||||
BackendVertexInputState& out) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||
Bool isLong = false);
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
|
||||
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||
//
|
||||
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||
// backend is in another process and its cache entry has no address a client could
|
||||
// store, so the memo has to live on the side that owns the pointee.
|
||||
//
|
||||
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||
struct VaoBackendMemos {
|
||||
// Whose memos these are. The identity table can recycle a slot for a different
|
||||
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||
// the contents are this object's and not its predecessor's.
|
||||
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Hash = 0;
|
||||
Uint32 HashConfigVersion = ~0u;
|
||||
const void* State = nullptr;
|
||||
Uint64 StateEpoch = 0;
|
||||
Uint32 StateConfigVersion = ~0u;
|
||||
};
|
||||
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||
// someone else's.
|
||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#endif
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Physical-device format capabilities never change during this factory's
|
||||
// lifetime. The wire path rebuilds layouts without the legacy VAO memo.
|
||||
mutable UnorderedMap<VkFormat, Bool> m_wireVertexFormatSupport;
|
||||
#endif
|
||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||
// invalidates references to stored values - and so does ERASE, which shifts
|
||||
// the rest of the probe cluster into the hole and therefore moves entries
|
||||
// other than the erased one. The draw path (and the VAOs' state-pointer
|
||||
// memos) hold entry pointers across both; only the unique_ptr cell moves,
|
||||
// never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
||||
// their heap-allocated entry (stable across map insert/rehash by
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// 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
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
//
|
||||
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
static inline Uint64 s_evictionEpochSource = 0;
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,6 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "MG_Pipe/MGPipeTypes.h"
|
||||
#include <unordered_map>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class BufferKind : Uint8 {
|
||||
@@ -35,9 +31,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
||||
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
Bool transientPersistentMapping = false;
|
||||
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
|
||||
// carries the transform feedback usage so capture targets can bind directly.
|
||||
Bool transformFeedbackUsageEnabled = false;
|
||||
};
|
||||
|
||||
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
|
||||
@@ -61,33 +54,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
||||
Bool persistentMapped = false;
|
||||
|
||||
// Bumped from a manager-wide counter every time anything that decides which
|
||||
// BufferSlice an Acquire*Slice call hands back changes: storage created or
|
||||
// released, a full re-upload becoming due, a promotion/demotion between
|
||||
// resident and streamed storage, or a new per-frame arena slice. Callers that
|
||||
// memoise a resolved slice compare this to prove the memo still describes the
|
||||
// buffer. The counter is manager-wide (never per-resource) so a freshly
|
||||
// created resource - including one that replaces a destroyed resource at the
|
||||
// same address - can never reproduce a value some memo already holds. 0 means
|
||||
// "no slice has ever been handed out", which no memo can match.
|
||||
Uint64 sliceEpoch = 0;
|
||||
|
||||
// Cached transient (streaming) slice for the current frame.
|
||||
BufferSlice transientSlice{};
|
||||
Uint64 transientFrameSerial = 0;
|
||||
Uint64 transientChangeSerial = 0;
|
||||
VkDeviceSize transientSize = 0;
|
||||
|
||||
// Streaming re-copies the whole store into the per-frame arena on every
|
||||
// frame, which is right for genuinely per-frame data but pure waste for a
|
||||
// Dynamic-hinted buffer the app stopped touching. After the content
|
||||
// survives kStreamedPromotionStreak frame boundaries unchanged it is
|
||||
// promoted to resident storage (one final upload, then zero per-frame
|
||||
// cost); the first content change demotes it back to streaming, and the
|
||||
// streaming path's existing downgrade releases the resident store.
|
||||
Uint32 unchangedStreak = 0;
|
||||
Bool promotedResident = false;
|
||||
Uint64 promotedChangeSerial = 0;
|
||||
};
|
||||
|
||||
// Supplies a command buffer that is recording and outside any render pass,
|
||||
@@ -103,48 +74,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Registered before caps publication; the initialized renderer owns the storage.
|
||||
static void RegisterWireResourceOps();
|
||||
// Transport resources are owned by their complete wire handle, never by a
|
||||
// frontend BufferObject. Acquires expose the full GPU store, without a CPU
|
||||
// pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes.
|
||||
Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice);
|
||||
Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst);
|
||||
Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const BufferSlice& dst);
|
||||
// P7 A.1, the sub-word half of the one above: the same copy for a window whose start
|
||||
// or end does not land on a four-byte boundary. The read of the APPLICATION's store is
|
||||
// rounded OUT to whole words and clamped to the store's end, so the widened window can
|
||||
// never touch a byte the application does not own; it lands in a transient staging
|
||||
// slice, and only the staging -> dst shift is sub-word - inside our own arena, where an
|
||||
// unaligned region cannot alias anything else. vkCmdCopyBuffer places no alignment rule
|
||||
// on a region's offsets or size (unlike vkCmdUpdateBuffer / vkCmdFillBuffer), so the
|
||||
// shift is a plain legal copy. `dstSkip` is the byte inside `dst` the window starts at.
|
||||
// This retires `uniform-buffer-byte-tail`.
|
||||
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
|
||||
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
|
||||
// Resource-op entry points. All run on the server apply owner.
|
||||
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
|
||||
void RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
|
||||
const void* initialBytes);
|
||||
void WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
#endif
|
||||
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the present-less
|
||||
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
|
||||
// superseded blocks: those are still named by this frame's slices (see the
|
||||
// definition), and only a frame rewind retires them.
|
||||
void CollectAllDeferredReleases();
|
||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||
void NotifyDeviceIdle();
|
||||
// A frame slot's submission fence has been waited: every serial up to
|
||||
@@ -157,27 +89,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
|
||||
VkDeviceSize alignment, BufferSlice& outSlice);
|
||||
|
||||
// The descriptor a shader storage block gets when the program declares it and the
|
||||
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
|
||||
// legal state - the block simply has no store, so reads are undefined and writes go
|
||||
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
|
||||
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
|
||||
// buffer, created once and shared by every unbound binding: bindings that are only
|
||||
// declared (the case this exists for) never touch it, and one that is actually read
|
||||
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
|
||||
// that indexes past it.
|
||||
BufferSlice AcquireUnboundStorageDescriptor();
|
||||
|
||||
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
|
||||
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
|
||||
// it has no GL buffer attached. Both are legal GL states that make a fetch return
|
||||
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
|
||||
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
|
||||
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
|
||||
// and the descriptor is a VkBufferView, not a buffer - is built by
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
@@ -197,24 +108,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Immediate ops, dispatched from the frontend BufferBackendOps table.
|
||||
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
|
||||
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
|
||||
void OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, DataPtr data);
|
||||
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> appAccess);
|
||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||
|
||||
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
||||
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
|
||||
// memo was taken means no buffer this manager owns changed which slice it hands
|
||||
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Bumped every time this manager destroys a WIRE store's VkBuffer (see
|
||||
// m_wireStoreDestroyEpoch). Unchanged since a memo was taken means no VkBuffer handle
|
||||
// that memo names can have been freed and re-minted in between, which is the one
|
||||
// fact a handle-keyed memo of wire descriptors needs and cannot read off the handle.
|
||||
Uint64 GetWireStoreDestroyEpoch() const { return m_wireStoreDestroyEpoch; }
|
||||
#endif
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
@@ -222,121 +120,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Busy = potentially referenced by GPU work that has not been fenced yet
|
||||
// (including commands recorded for the current, unsubmitted frame).
|
||||
Bool IsResourceBusy(const VkBufferResource& resource) const;
|
||||
// Hand the manager a buffer to destroy once the frame that recorded commands
|
||||
// naming it has completed. The renderer's blit path needs a device-local
|
||||
// scratch store for one recorded operation, which cannot be a stack local:
|
||||
// the glBlitFramebuffer that records the commands returns long before the
|
||||
// command buffer is submitted.
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
|
||||
private:
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
struct WireBufferResource {
|
||||
VkBufferObject buffer;
|
||||
Uint64 size = 0;
|
||||
Uint64 lastUseSerial = 0;
|
||||
// P7 wave 2 package B3: the submission expected to carry the most recent GPU use
|
||||
// of this buffer - the busy predicate's DEFENCE term, not its guarantee (see
|
||||
// WriteWireBuffer). IsSubmitIndexComplete polls the real fence and reports an
|
||||
// unsubmitted index as incomplete, but the stamp is taken before the draw is
|
||||
// recorded and a mid-draw flush can submit it without the draw.
|
||||
Uint64 lastUseSubmitIndex = 0;
|
||||
Bool gpuWritesPending = false;
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
Vector<Range1D> stagedCoverage;
|
||||
};
|
||||
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
|
||||
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
|
||||
}
|
||||
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
|
||||
|
||||
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
|
||||
// BOUNDARY.
|
||||
//
|
||||
// RespecifyWireBuffer - which is every glBufferData that crosses the wire - orphans the
|
||||
// old store unconditionally, because unlike OnRespecify it has no shadow to upload in
|
||||
// place from and no cheap way to know the client is re-sending the same size. The orphan
|
||||
// is legitimate (M1 measured the conditional-orphan mirror: 6701 live against 6702).
|
||||
// What was missing is the RECLAIM. DeferRelease parks into m_deferredBufferReleases,
|
||||
// whose only sweep is CollectDeferredReleases from a frame boundary, and on a pbuffer
|
||||
// replay the server sees ONE present record for the whole run - so on
|
||||
// minecraft-1.21.4-fabric-iris-bsl-esc-menu-854 the buckets held 25,923 dead stores
|
||||
// against 28 live wire buffers, one memfd mapping each, and the server died in scudo's
|
||||
// secondary allocator.
|
||||
//
|
||||
// So a wire store is parked HERE instead, with the facts that say when it is dead, and
|
||||
// THE DEFER PATH ITSELF RECLAIMS - every park sweeps, and the frame boundary is only one
|
||||
// more sweep point (CollectDeferredReleases), never the one this depends on:
|
||||
//
|
||||
// * `lastUseSerial == 0` - read BEFORE RespecifyWireBuffer zeroes it. Zero means no
|
||||
// GPU command has named the store since it was minted or since the last host-access
|
||||
// wait proved every recorded command complete (WaitForWireBufferHostAccess); every
|
||||
// path that hands the store to the GPU stamps m_frameSerial, which starts at 1. Such
|
||||
// a store is destroyed at the park and never enters the list.
|
||||
// * `submitIndex` - the renderer's GetSyncPointSubmitIndex() at park time, which is by
|
||||
// construction the LAST submission that can name this store: every command that
|
||||
// names it was recorded before the park (the record left behind a bumped slice
|
||||
// epoch, so no memo can hand it out again), and a recorded command is either already
|
||||
// submitted (<= m_submitCounter) or in the batch that becomes m_submitCounter + 1.
|
||||
// IsSubmitIndexComplete(submitIndex) is a fence observation, so this is the gate
|
||||
// that empties the set MID-FRAME - the shape CollectWireObjects already uses for
|
||||
// the wire image/view tables.
|
||||
// * THE RENDERER IS IDLE - IsSubmitIndexComplete(GetSyncPointSubmitIndex()): nothing
|
||||
// is recorded and every submission has retired. Then every parked store is dead,
|
||||
// including one tagged for a batch that was abandoned rather than submitted (a
|
||||
// minimized Present drops its recording), whose own index may never complete.
|
||||
//
|
||||
// THE FRAME-SERIAL FLOOR IS DELIBERATELY NOT A PROOF HERE. It cannot move inside a
|
||||
// frame (NotifyFrameSerialComplete refuses the current serial), so it frees nothing in
|
||||
// the one-present replay this exists for; the submit index above is the fact that CAN
|
||||
// move mid-frame, and it is a fence observation rather than a count.
|
||||
//
|
||||
// AND EVERY DESTROY HERE HAPPENS MID-FRAME, which the memos above this manager were not
|
||||
// written for. UniformManager's descriptor memos are keyed on the VkBuffer HANDLE, and
|
||||
// before M2 a wire store only ever died at the same boundary that clears those memos
|
||||
// (UniformManager::BeginFrame, from Present or a drain). A store destroyed here can
|
||||
// have its handle value re-minted by the next Create - a heap pointer under lavapipe -
|
||||
// while a memo still maps that handle to a descriptor set baked to the dead store's
|
||||
// memory: silent wrong bytes, no Fatal (ID-P7-43). m_wireStoreDestroyEpoch is the
|
||||
// fact the memos fold in: every wire-store destroy bumps it, so a memo taken before
|
||||
// the destroy cannot match after it. Deliberately NOT m_sliceEpochCounter, which every
|
||||
// WriteWireBuffer bumps and which would defeat the memo on every glBufferSubData.
|
||||
struct DeferredWireRelease {
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseSerial = 0;
|
||||
Uint64 submitIndex = 0;
|
||||
// Cached: GetSize() is gone once the object is destroyed, and the watermark's
|
||||
// running total has to stay exact as entries leave.
|
||||
Uint64 bytes = 0;
|
||||
};
|
||||
// Park a wire store, sweep, then hold the watermark. `lastUseSerial` is the releasing
|
||||
// resource's, captured before the caller resets it.
|
||||
void DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial);
|
||||
// Destroy every parked store proven dead by the rules above. Returns how many.
|
||||
SizeT SweepDeferredWireReleases();
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (Config.h has the semantics): when the parked bytes
|
||||
// still exceed the budget after a sweep, take the sync point WaitForWireBufferHostAccess
|
||||
// takes - flush what is recorded, wait for it - and sweep again, which retires every
|
||||
// parked store, because none can be tagged past the sync point it just waited out.
|
||||
void EnforceWireDeferredWatermark();
|
||||
// ...and the same sync point when more than this many stores are parked, whatever their
|
||||
// bytes (see the definition for the measurement). Not a knob: it bounds an object count
|
||||
// the byte budget cannot see, and MOBILEGL_IPC_WIRE_DEFERRED_MB=0 disables it too.
|
||||
static constexpr SizeT kWireDeferredCountCeiling = 1024;
|
||||
// Teardown: the caller has proven the device idle (Shutdown / RecreateTransientArenas).
|
||||
void DestroyAllDeferredWireReleases();
|
||||
// THE one Fatal{ResourceUnavailable, "buffer-write-sync"} site (rule I: no second abort
|
||||
// for the census to count), shared by the host write that cannot wait for the GPU and
|
||||
// the watermark that cannot. `site` says which.
|
||||
[[noreturn]] static void WireBufferSyncFatal(const char* site);
|
||||
// The wbuf[] gauges (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs). The peaks are
|
||||
// taken at the two points the numbers can rise - a park and a mint - and published when
|
||||
// the stats channel is on; MagmaWireReclaimScenario reads them off the server's line.
|
||||
void NoteWireStorePeaks();
|
||||
void PublishWireReclaimGauges();
|
||||
#endif
|
||||
Bool InitializeTransientArenas();
|
||||
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
|
||||
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
@@ -347,57 +132,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
|
||||
// Record a staging-slice copy into the resident storage, ordered against
|
||||
// in-flight and already-recorded GPU work.
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, const void* data,
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
|
||||
SizeT offset, SizeT size);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size);
|
||||
#endif
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyAllDeferredReleases();
|
||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||
void ReleaseAllLiveResources();
|
||||
// See VkBufferResource::sliceEpoch.
|
||||
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
|
||||
|
||||
VkBufferManagerInitInfo m_initInfo{};
|
||||
BufferArena m_transientUploadArena;
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
// with the manager.
|
||||
VkBufferObject m_unboundStorageBuffer;
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
|
||||
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
|
||||
// the whole point is that these entries do not wait for a frame slot to come round.
|
||||
Vector<DeferredWireRelease> m_deferredWireReleases;
|
||||
// Bytes currently parked in that list, kept exact so the watermark needs no walk.
|
||||
Uint64 m_deferredWireBytes = 0;
|
||||
// Live VkBuffers this arm owns: the stores held by m_wireBuffers plus the parked ones.
|
||||
// Maintained rather than counted, because the publish runs on every park.
|
||||
Uint64 m_wireStoreCount = 0;
|
||||
// Run maxima of the two numbers above and the watermark's sync count, for the gauges.
|
||||
Uint64 m_wireStoreCountPeak = 0;
|
||||
Uint64 m_deferredWireBytesPeak = 0;
|
||||
Uint64 m_wireDeferredSyncs = 0;
|
||||
// See GetWireStoreDestroyEpoch and the DeferredWireRelease comment. Bumped on every
|
||||
// path that destroys a wire store's VkBuffer, and never reset (not even by Shutdown),
|
||||
// for m_sliceEpochCounter's reason: a memo taken before a re-initialize must not match
|
||||
// a handle minted after it.
|
||||
Uint64 m_wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
|
||||
SizeT m_liveResourcesLastPruned = 0;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
Uint64 m_frameSerial = 1;
|
||||
Uint64 m_completedSerialFloor = 0;
|
||||
// Never reset (not even by Shutdown): a value handed to a resource must stay
|
||||
// unique for the process, or a memo taken before a re-initialize could match
|
||||
// a different resource's state after it.
|
||||
Uint64 m_sliceEpochCounter = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult result =
|
||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
m_allocator = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_allocation = nullptr;
|
||||
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
m_mappedData = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool wasMapped = IsMapped();
|
||||
void* mapped = wasMapped ? m_mappedData : Map();
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||
if (flushResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
if (!wasMapped) {
|
||||
Unmap();
|
||||
}
|
||||
@@ -170,10 +170,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
||||
|
||||
BufferSlice slice{};
|
||||
slice.buffer = m_buffer;
|
||||
slice.offset = offset;
|
||||
slice.size = resolvedSize;
|
||||
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
||||
return slice;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -48,20 +48,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkBuffer GetHandle() const { return m_buffer; }
|
||||
VkDeviceSize GetSize() const { return m_size; }
|
||||
// Inline: runs on the per-draw acquire path (a resident buffer bind is a
|
||||
// GetSlice per binding), where an out-of-line call was measurable.
|
||||
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const {
|
||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
||||
|
||||
BufferSlice slice{};
|
||||
slice.buffer = m_buffer;
|
||||
slice.offset = offset;
|
||||
slice.size = resolvedSize;
|
||||
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
||||
return slice;
|
||||
}
|
||||
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
|
||||
void* GetMappedData() const { return m_mappedData; }
|
||||
Bool IsMapped() const { return m_mappedData != nullptr; }
|
||||
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
||||
|
||||
@@ -8,80 +8,15 @@
|
||||
|
||||
#include "VkClearManager.h"
|
||||
|
||||
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
|
||||
// clear key's layer span has to be the same one the render pass builds its attachment view from.
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||
return target >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
target <= TextureUploadTarget::CubeMapNegativeZ;
|
||||
}
|
||||
|
||||
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha) {
|
||||
VkClearColorValue clearValue{};
|
||||
switch (payload.colorEncoding) {
|
||||
case ClearColorEncoding::Int:
|
||||
clearValue.int32[0] = payload.colorInt.x();
|
||||
clearValue.int32[1] = payload.colorInt.y();
|
||||
clearValue.int32[2] = payload.colorInt.z();
|
||||
clearValue.int32[3] = formatLacksAlpha ? 1 : payload.colorInt.w();
|
||||
break;
|
||||
case ClearColorEncoding::Uint:
|
||||
clearValue.uint32[0] = payload.colorUint.x();
|
||||
clearValue.uint32[1] = payload.colorUint.y();
|
||||
clearValue.uint32[2] = payload.colorUint.z();
|
||||
clearValue.uint32[3] = formatLacksAlpha ? 1u : payload.colorUint.w();
|
||||
break;
|
||||
case ClearColorEncoding::Float:
|
||||
clearValue.float32[0] = payload.color.x();
|
||||
clearValue.float32[1] = payload.color.y();
|
||||
clearValue.float32[2] = payload.color.z();
|
||||
clearValue.float32[3] = formatLacksAlpha ? 1.0f : payload.color.w();
|
||||
break;
|
||||
}
|
||||
return clearValue;
|
||||
}
|
||||
|
||||
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat) {
|
||||
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 (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
|
||||
// linearly in an sRGB format and must pass through untouched.
|
||||
const auto toLinear = [](Float encoded) {
|
||||
const Float value = std::clamp(encoded, 0.0f, 1.0f);
|
||||
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
|
||||
};
|
||||
payload.color = FloatVec4(toLinear(payload.color.x()), toLinear(payload.color.y()),
|
||||
toLinear(payload.color.z()), payload.color.w());
|
||||
}
|
||||
|
||||
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
|
||||
switch (payload.colorEncoding) {
|
||||
case ClearColorEncoding::Int:
|
||||
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
|
||||
break;
|
||||
case ClearColorEncoding::Uint:
|
||||
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
|
||||
break;
|
||||
case ClearColorEncoding::Float:
|
||||
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
|
||||
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
|
||||
}
|
||||
@@ -105,13 +40,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
|
||||
// the entry - so a layered cube map's glClear reached one face and the other five were lost
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
static Uint32 ResolveAttachmentLayerCount(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
@@ -127,30 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -180,19 +93,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -202,15 +105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||
dst.mask |= src.mask;
|
||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
// The whole colour story travels together (same rule as
|
||||
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||
// payload carries its value in colorInt/colorUint and its branch selector
|
||||
// in colorEncoding - dropping them here would leave the pending clear
|
||||
// reading as an all-zero float one.
|
||||
dst.color = src.color;
|
||||
dst.colorEncoding = src.colorEncoding;
|
||||
dst.colorInt = src.colorInt;
|
||||
dst.colorUint = src.colorUint;
|
||||
}
|
||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
dst.depth = src.depth;
|
||||
@@ -232,7 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_pendingClears.erase(key);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
@@ -322,14 +216,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const PendingClearKey key = MakePendingClearKey(texture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -343,17 +234,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -361,11 +245,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -381,9 +260,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||
@@ -411,9 +287,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!LockTextureLocked(key, outTexture)) {
|
||||
@@ -452,11 +325,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
@@ -476,9 +345,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -495,7 +361,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it != m_pendingClears.end()) {
|
||||
m_pendingClears.erase(it);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -24,41 +23,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 stencil{};
|
||||
};
|
||||
|
||||
// A colour clear reaches us from one of glClear/ClearBufferfv, ClearBufferiv or
|
||||
// ClearBufferuiv, and Vulkan reads VkClearColorValue's union according to the destination
|
||||
// image's format rather than converting between the members - a float written where an
|
||||
// integer format is expected is reinterpreted bit for bit, not rounded. Remember which entry
|
||||
// point supplied the value so the member written when the clear is materialized matches.
|
||||
enum class ClearColorEncoding : Uint8 { Float, Int, Uint };
|
||||
|
||||
struct ClearAttachmentPayload {
|
||||
GLbitfield mask = 0;
|
||||
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
ClearColorEncoding colorEncoding = ClearColorEncoding::Float;
|
||||
IntVec4 colorInt = IntVec4(0, 0, 0, 0);
|
||||
UintVec4 colorUint = UintVec4(0u, 0u, 0u, 0u);
|
||||
Float depth = 1.0f;
|
||||
Uint32 stencil = 0;
|
||||
};
|
||||
|
||||
// Builds the clear value for `payload` in the union member its encoding calls for.
|
||||
// `formatLacksAlpha` applies GL's rule that a format without an alpha channel reads as one,
|
||||
// expressed in whichever type matches (GL 4.6 core 15.2.3).
|
||||
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
|
||||
|
||||
// Applies that same rule in place, for the paths that have to bake it into the payload before
|
||||
// the destination is known.
|
||||
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
|
||||
|
||||
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
|
||||
// function to whatever value it is handed. Every other write path in this backend goes through
|
||||
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
|
||||
// therefore stores the raw value GL asked for. Rewrites `payload` to the linear colour whose
|
||||
// encoding is that raw value, so a direct image clear of an sRGB destination agrees with them.
|
||||
// A no-op for every other format, for integer clear encodings, and when GL is doing the
|
||||
// encoding itself.
|
||||
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
|
||||
|
||||
struct PendingClearKey {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
@@ -114,8 +85,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
@@ -150,19 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
public:
|
||||
// Lock-free probe for the consecutive-draw fast path: any pending clear
|
||||
// forces the full SetupDraw path (which materializes/consumes it).
|
||||
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
|
||||
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
|
||||
// read it before taking the lock: during draw batches the pending set
|
||||
// is almost always empty, so this turns several locked map probes per
|
||||
// draw into one relaxed load.
|
||||
std::atomic<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <unordered_map>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -43,11 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct TrackedAttachmentLayoutInfo {
|
||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||
// May dangle once the texture dies - compare only, never dereference.
|
||||
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
Uint32 textureMipLevel = 0;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
@@ -101,42 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
}
|
||||
// Move ASSIGNMENT, not just construction. The move constructor above and the
|
||||
// destructor below each independently suppress the implicit one, which left the
|
||||
// type move-constructible but not move-assignable - and therefore not swappable,
|
||||
// which std::swap(pair&, pair&) requires. That was invisible while UnorderedMap
|
||||
// only ever move-CONSTRUCTED an element into a fresh slot. ska::flat_hash_map
|
||||
// probes robin-hood: inserting swaps the entry being placed against the one
|
||||
// already sitting in the slot whenever it has travelled further from its desired
|
||||
// position, so the mapped type has to be swappable or the table fails to
|
||||
// instantiate at all.
|
||||
//
|
||||
// SWAP SEMANTICS, exactly like the move constructor: this does not release the
|
||||
// destination's handles, it parks them in `that`, which destroys them when it
|
||||
// dies. That is correct for the only caller - std::swap, whose temporary expires
|
||||
// immediately - and it is what keeps the three-move sequence from destroying a
|
||||
// live render pass. It is NOT correct for a hand-written `a = std::move(b)` where
|
||||
// `a` held live handles and `b` outlives the statement: those handles would then
|
||||
// survive until `b` dies. There is no such caller; add a destroy-then-steal
|
||||
// assignment before writing one.
|
||||
RenderPassEntry& operator=(RenderPassEntry&& that) noexcept {
|
||||
if (this != &that) {
|
||||
std::swap(hash, that.hash);
|
||||
std::swap(renderPass, that.renderPass);
|
||||
std::swap(framebuffer, that.framebuffer);
|
||||
std::swap(compatibilityHash, that.compatibilityHash);
|
||||
std::swap(pendingClearAttachments, that.pendingClearAttachments);
|
||||
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
|
||||
std::swap(attachmentCount, that.attachmentCount);
|
||||
std::swap(colorAttachmentCount, that.colorAttachmentCount);
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
std::swap(sampleCount, that.sampleCount);
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
RenderPassEntry(
|
||||
Uint64 hash,
|
||||
VkRenderPass renderpass,
|
||||
@@ -199,68 +157,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkRenderPassManager {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
||||
// everything keyed on them before any new pass can be created (the sweep and
|
||||
// the notification run back-to-back with no creation in between; observers
|
||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
||||
// scan, not one per dying pass. The wholesale paths
|
||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
||||
// every pipeline outright.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
||||
};
|
||||
|
||||
VkRenderPassManager(VkDevice device,
|
||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||
~VkRenderPassManager();
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool includePendingClear = true,
|
||||
Bool includeDefaultFboDepthStencil = true);
|
||||
// drawUsesDepthStencil: whether the operation about to run inside the pass
|
||||
// reads or writes the depth/stencil buffer (depth test or stencil test
|
||||
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
|
||||
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
|
||||
// default-FBO pass whose draws provably never touch depth/stencil is
|
||||
// created WITHOUT the depth attachment - on a tiler that skips the whole
|
||||
// depth tile load AND store. The flavor only escalates: once a pass with
|
||||
// depth is active, later depth-less draws keep using it, and a depth-using
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
Bool includePendingClear = true);
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -283,20 +192,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
|
||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||
// image recreation.
|
||||
Uint64 m_renderbufferImageEpoch = 1;
|
||||
|
||||
public:
|
||||
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
|
||||
// snapshots include it so an attachment respecify forces a re-resolve.
|
||||
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
|
||||
|
||||
private:
|
||||
|
||||
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
|
||||
// framebuffer state is provably unchanged since the last resolution, the active render pass
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
@@ -304,36 +205,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||
Bool m_rpFastValid = false;
|
||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||
// the same number of version bumps would otherwise compare equal (both count
|
||||
// from 0), serving the dead framebuffer's pass to the new object.
|
||||
Uint64 m_rpFastFboLifetimeId = 0;
|
||||
Uint16 m_rpFastFboVersion = 0;
|
||||
Uint32 m_rpFastSwapchainIndex = 0;
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
// Whether the memoized entry carries a depth/stencil attachment; a
|
||||
// default-FBO resolution whose effective depth request differs must
|
||||
// miss the memo (the depth-less/depth-full flavors hash differently).
|
||||
Bool m_rpFastHadDepthStencil = false;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
||||
// expired. Dead resources age past every in-flight frame before Destroy
|
||||
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
||||
// for frames-in-flight frames after the GL object dies.
|
||||
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
||||
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
// UNORM reinterpretation of an sRGB image, used as the attachment view while
|
||||
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -341,8 +224,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
Int samples = 0;
|
||||
// m_frameCounter value at which the weak reference was first seen expired.
|
||||
Uint64 deadSinceFrame = kNeverObservedDead;
|
||||
|
||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||
};
|
||||
@@ -360,56 +241,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
||||
// until enough frame boundaries have passed that no in-flight command buffer
|
||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
||||
struct DeferredRenderbufferRelease {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
|
||||
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||
// materializes the source's pending clear, which looks that same resource up again. Growing
|
||||
// an open-addressed table relocates every element, so the cached pointer went on to name
|
||||
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
|
||||
// out at "source image layout is undefined", silently dropping the blit -
|
||||
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
|
||||
// iterations that grew the table.
|
||||
//
|
||||
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||
// ordering rule. m_textureResources is node-based for the same reason.
|
||||
//
|
||||
// The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
|
||||
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
|
||||
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
|
||||
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
|
||||
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
|
||||
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
|
||||
// That narrows the insert hazard; it does not touch the erase one.)
|
||||
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
|
||||
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
void CollectRenderbufferGarbage();
|
||||
// Frame-boundary margin after which a resource last referenced by a retired
|
||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
||||
Uint64 RetireAgeFrames() const;
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
|
||||
@@ -15,192 +15,12 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// An accessor view over the wire's POD values, not a GL object with constructor,
|
||||
// lifetime ID, allocator hooks or mutation callbacks. Both sources below use
|
||||
// one sampler policy, including format-sensitive border-color conversion.
|
||||
struct SamplerParametersSource {
|
||||
const SamplerParameters& Params;
|
||||
SamplerWrapMode GetWrapS() const { return Params.wrapS; }
|
||||
SamplerWrapMode GetWrapT() const { return Params.wrapT; }
|
||||
SamplerWrapMode GetWrapR() const { return Params.wrapR; }
|
||||
SamplerFilterMode GetMinFilter() const { return Params.minFilter; }
|
||||
SamplerFilterMode GetMagFilter() const { return Params.magFilter; }
|
||||
SamplerMipmapMode GetMipmapMode() const { return Params.mipmapMode; }
|
||||
Float GetMinLod() const { return Params.minLod; }
|
||||
Float GetMaxLod() const { return Params.maxLod; }
|
||||
Float GetLodBias() const { return Params.lodBias; }
|
||||
Float GetMaxAnisotropy() const { return Params.maxAnisotropy; }
|
||||
SamplerCompareMode GetCompareMode() const { return Params.compareMode; }
|
||||
SamplerCompareFunc GetSamplerCompareFunc() const { return Params.compareFunc; }
|
||||
const FloatVec4& GetBorderColor() const { return Params.borderColor; }
|
||||
const IntVec4& GetBorderColorI() const { return Params.borderColorI; }
|
||||
const UintVec4& GetBorderColorUI() const { return Params.borderColorUI; }
|
||||
// Cache identity is entirely content-based; these fields are diagnostics.
|
||||
Uint GetExternalIndex() const { return 0; }
|
||||
Uint16 GetVersion() const { return 0; }
|
||||
};
|
||||
|
||||
template <class SamplerSource>
|
||||
Bool UsesBorderColor(const SamplerSource& sampler) {
|
||||
#else
|
||||
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
}
|
||||
|
||||
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
|
||||
// family and an integer family and requires the sampler's choice to match the image view's
|
||||
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
|
||||
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
|
||||
enum class BorderColorDomain {
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger
|
||||
};
|
||||
|
||||
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return BorderColorDomain::SignedInteger;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return BorderColorDomain::UnsignedInteger;
|
||||
default:
|
||||
return BorderColorDomain::Float;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
|
||||
// format in which texture components are stored. For signed and unsigned normalized
|
||||
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
|
||||
// floating-point and integer formats, border values are clamped to the representable range of
|
||||
// the format." Every clause of that sentence is a real case here - the clamp is not just the
|
||||
// normalized one.
|
||||
//
|
||||
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
|
||||
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
|
||||
// so a negative border on them must come back as 0 rather than as a negative number the
|
||||
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
|
||||
struct FloatBorderRange {
|
||||
Bool clamped = true;
|
||||
Float minValue = 0.0f;
|
||||
Float maxValue = 1.0f;
|
||||
};
|
||||
|
||||
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
return {false, 0.0f, 0.0f};
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
return {true, -65504.0f, 65504.0f};
|
||||
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
|
||||
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
|
||||
// that matters for correctness is the lower one, and a single conservative upper bound
|
||||
// costs nothing a real border colour will ever notice.
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
return {true, 0.0f, 64512.0f};
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
return {true, 0.0f, 65408.0f};
|
||||
default:
|
||||
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Per-component representable range of an integer texture format, as Int64 so that the whole
|
||||
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
|
||||
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
|
||||
// whose alpha is narrower than its colour channels.
|
||||
struct IntegerBorderRange {
|
||||
Int64 rgbMin = 0;
|
||||
Int64 rgbMax = 0;
|
||||
Int64 alphaMin = 0;
|
||||
Int64 alphaMax = 0;
|
||||
};
|
||||
|
||||
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
|
||||
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
return uniform(-128, 127);
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
return uniform(-32768, 32767);
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
return uniform(0, 255);
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
return uniform(0, 65535);
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
return uniform(0, 4294967295LL);
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return {0, 1023, 0, 3};
|
||||
default:
|
||||
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
|
||||
// clamp that cannot alter a value the GL entry points could have carried.
|
||||
return uniform(-2147483648LL, 2147483647LL);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsDepthTextureFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
@@ -221,43 +41,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return std::fabs(lhs - rhs) <= 1e-6f;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMaxLod(const SamplerSource& sampler) {
|
||||
#else
|
||||
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
|
||||
return 0.0f;
|
||||
}
|
||||
return sampler.GetMaxLod();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) {
|
||||
#else
|
||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||
#endif
|
||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||
}
|
||||
|
||||
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
|
||||
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
|
||||
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
|
||||
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
|
||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) {
|
||||
#else
|
||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
||||
#endif
|
||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
|
||||
@@ -267,20 +60,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
m_customBorderColorSupported = initInfo.customBorderColorSupported;
|
||||
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
|
||||
#else
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering) const {
|
||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||
if (forceNearestFiltering) return 1.0f;
|
||||
@@ -304,54 +89,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
m_customBorderColorSupported = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; destroy samplers whose last use is far past every
|
||||
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
|
||||
// double-free the handle; an evicted key that recurs simply re-creates
|
||||
// its sampler on the next miss.
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
|
||||
auto& entry = it->second;
|
||||
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler,
|
||||
#else
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
@@ -365,7 +111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
@@ -378,46 +124,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
const auto compareFunc = ResolveCompareFunc(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler,
|
||||
TextureInternalFormat format,
|
||||
#else
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
||||
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
|
||||
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
|
||||
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
|
||||
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format);
|
||||
#else
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
#endif
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
Bool forceNearestFiltering) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return it->second.handle;
|
||||
}
|
||||
|
||||
@@ -425,9 +144,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
|
||||
? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||
@@ -438,25 +156,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.compareOp = ToVkCompareOp(sampler.GetSamplerCompareFunc());
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
||||
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = borderColor.color;
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
|
||||
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
|
||||
// customBorderColorWithoutFormat was required alongside customBorderColors at device
|
||||
// creation - a GL sampler object has no idea which texture it will be paired with.
|
||||
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
|
||||
if (borderColor.isCustom) {
|
||||
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
|
||||
customBorderColorInfo.customBorderColor = borderColor.customValue;
|
||||
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
|
||||
customBorderColorInfo.pNext = samplerInfo.pNext;
|
||||
samplerInfo.pNext = &customBorderColorInfo;
|
||||
}
|
||||
|
||||
VkSampler vkSampler = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
|
||||
|
||||
@@ -464,29 +169,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.handle = vkSampler;
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.usesCustomBorderColor = borderColor.isCustom;
|
||||
if (entry.usesCustomBorderColor) {
|
||||
++m_customBorderColorSamplerCount;
|
||||
}
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
|
||||
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
|
||||
forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
|
||||
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
|
||||
}
|
||||
@@ -542,156 +228,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const SamplerSource& sampler, TextureInternalFormat format) const {
|
||||
#else
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
#endif
|
||||
ResolvedBorderColor resolved{};
|
||||
SamplerCompareFunc VkSamplerManager::ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
if (sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture &&
|
||||
IsDepthTextureFormat(texture.GetFormat()) && compareFunc == SamplerCompareFunc::Always) {
|
||||
return SamplerCompareFunc::LessEqual;
|
||||
}
|
||||
|
||||
return compareFunc;
|
||||
}
|
||||
|
||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
const auto format = texture.GetFormat();
|
||||
#endif
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
const auto& borderColor = texture.GetBorderColor();
|
||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||
|
||||
if (domain != BorderColorDomain::Float) {
|
||||
// An integer image view REQUIRES an integer border colour, whatever the value is - even
|
||||
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
|
||||
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
|
||||
// representation (a plain cast) is as good an answer as any.
|
||||
//
|
||||
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
|
||||
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
|
||||
//
|
||||
// Which representation to start from is the TEXTURE's domain, not the entry-point form
|
||||
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
|
||||
// an integer internal data type and does not define a sign conversion between the two
|
||||
// integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
|
||||
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
|
||||
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
|
||||
// the representation by the FORM instead would answer 0 here, which is a defensible
|
||||
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
|
||||
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
|
||||
// agreement decides it.
|
||||
const auto range = ResolveIntegerBorderRange(format);
|
||||
const auto& borderColorI = sampler.GetBorderColorI();
|
||||
const auto& borderColorUI = sampler.GetBorderColorUI();
|
||||
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
|
||||
Int64 clamped[4];
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
|
||||
: static_cast<Int64>(borderColorI[channel]);
|
||||
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
|
||||
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
|
||||
clamped[channel] = std::clamp(raw, low, high);
|
||||
}
|
||||
|
||||
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
|
||||
// the palette entry it clamps to rather than missing every one of them.
|
||||
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
|
||||
if (allZeroRgb && clamped[3] == 0) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (allZeroRgb && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (domain == BorderColorDomain::UnsignedInteger) {
|
||||
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
|
||||
} else {
|
||||
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
// No custom colour available: pick the nearest of the three integer palette entries
|
||||
// rather than always answering transparent black, which is what turned an integer border
|
||||
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
|
||||
const Bool opaque = clamped[3] != 0;
|
||||
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
|
||||
// format, so it is clamped to that format's representable range first. Without the clamp the
|
||||
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
|
||||
// and fell through to transparent black - every border texel sampled 0 where the test wanted
|
||||
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
|
||||
// 32-bit float formats are unclamped.
|
||||
FloatVec4 borderColor = sampler.GetBorderColor();
|
||||
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
|
||||
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.y(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.z(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.w(), range.minValue, range.maxValue));
|
||||
}
|
||||
|
||||
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
|
||||
if (IsDepthTextureFormat(format)) {
|
||||
if (isDepthTexture) {
|
||||
if (NearlyEqual(borderColor.x(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 0.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
|
||||
NearlyEqual(borderColor.z(), 0.0f);
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
|
||||
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
}
|
||||
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
resolved.customValue.float32[0] = borderColor.x();
|
||||
resolved.customValue.float32[1] = borderColor.y();
|
||||
resolved.customValue.float32[2] = borderColor.z();
|
||||
resolved.customValue.float32[3] = borderColor.w();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Nearest of the three float palette entries. Transparent black stays the answer for a
|
||||
// transparent border, which is what the old unconditional fallback got right by accident.
|
||||
const Bool opaque = borderColor.w() >= 0.5f;
|
||||
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -28,116 +28,45 @@ public:
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
|
||||
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
|
||||
Bool customBorderColorSupported = false;
|
||||
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
|
||||
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
|
||||
// and falls back to the snapped predefined value once it is reached.
|
||||
Uint32 maxCustomBorderColorSamplers = 0;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
// viewLevelCount is the mip-level count of the image view this sampler will be paired
|
||||
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false,
|
||||
Uint32 viewLevelCount = 0);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The applier owns these values: effective bound/builtin sampler parameters and
|
||||
// the sampled texture view's format/range. No frontend object is constructed.
|
||||
VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
|
||||
TextureInternalFormat format, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0);
|
||||
#endif
|
||||
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
||||
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
||||
// anisotropy), so an app animating those would otherwise mint an unbounded
|
||||
// stream of never-destroyed VkSamplers and eventually exhaust the device's
|
||||
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
|
||||
// boundaries cannot be referenced by any in-flight command buffer (frames in
|
||||
// flight are single digits), and every descriptor set the GPU consumes is
|
||||
// written that same frame with live handles (the per-binding resolve memo and
|
||||
// descriptor-set reuse are both frame-reset), so destruction here needs no
|
||||
// fence wait. Self-gated: one counter bump and compare except on sweep
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
|
||||
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
|
||||
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
|
||||
//
|
||||
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
|
||||
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
|
||||
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
|
||||
// custom colour was affordable is decided before the key is built, not twice with a budget
|
||||
// change in between.
|
||||
struct ResolvedBorderColor {
|
||||
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
VkClearColorValue customValue{};
|
||||
Bool isCustom = false;
|
||||
};
|
||||
Bool forceNearestFiltering = false);
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
Uint externalIndex = 0;
|
||||
Uint16 version = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
|
||||
Bool usesCustomBorderColor = false;
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSampler GetOrCreateSamplerImpl(const SamplerSource& sampler, TextureInternalFormat format,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount);
|
||||
template <class SamplerSource>
|
||||
Uint64 BuildSamplerKey(const SamplerSource& sampler, Bool forceNearestFiltering,
|
||||
#else
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
|
||||
#endif
|
||||
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
ResolvedBorderColor ResolveBorderColor(const SamplerSource& sampler, TextureInternalFormat format) const;
|
||||
#else
|
||||
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
#endif
|
||||
static SamplerCompareFunc ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
|
||||
#else
|
||||
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
Bool m_customBorderColorSupported = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
|
||||
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
|
||||
// and Shutdown.
|
||||
Uint32 m_customBorderColorSamplerCount = 0;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,137 +10,24 @@
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape
|
||||
// MagmaPipeArms.h already gives this directory.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class ITextureObject;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct MGPipeResourceRecord;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
|
||||
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
|
||||
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
|
||||
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
|
||||
// because Vulkan devices rarely support the 3-channel layouts.
|
||||
//
|
||||
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
|
||||
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
|
||||
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
|
||||
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
|
||||
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
|
||||
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
|
||||
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
|
||||
// `.format` and ignore the rest.
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
|
||||
|
||||
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
||||
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
||||
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
|
||||
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
|
||||
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
|
||||
// level size into Vulkan image geometry has to move the count across first, and every GL-space
|
||||
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
|
||||
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
|
||||
//
|
||||
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
|
||||
// depth/layer count in z, which is where the Vulkan side expects it.
|
||||
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
return {glTexelSize.x(), 1, glTexelSize.y()};
|
||||
}
|
||||
return glTexelSize;
|
||||
}
|
||||
|
||||
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
|
||||
//
|
||||
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
|
||||
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
|
||||
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
|
||||
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
|
||||
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
|
||||
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
|
||||
// depending only on which path happens to consume it first - and the materialise path then POPS
|
||||
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
|
||||
// that split gets introduced; keep them the same function.
|
||||
//
|
||||
// Two shapes make this more than `size.z()`:
|
||||
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
|
||||
// just above), so z reads 1 and every layer above the first was silently dropped.
|
||||
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
|
||||
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
|
||||
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
|
||||
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
|
||||
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsLayered()) {
|
||||
return 1u;
|
||||
}
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
if (target == TextureTarget::TextureCubeMap) {
|
||||
return 6u;
|
||||
}
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
|
||||
// storage texture's (VkTextureManager::StorageTextureOf).
|
||||
//
|
||||
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
|
||||
// GetOrCreate*View entry point below expects values that have already been through here, and so
|
||||
// does everything that reads or copies an attachment directly. Both are identity on a plain
|
||||
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
|
||||
// and there is no second, view-only code path to keep in step.
|
||||
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
|
||||
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
|
||||
}
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
@@ -166,20 +53,6 @@ public:
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
||||
Uint32 frameCount = 0;
|
||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||
Bool imageFormatListSupported = false;
|
||||
// Union of shader stages sampled-read barriers may name on this device; the renderer
|
||||
// builds it from the enabled features because geometry/tessellation stage bits are
|
||||
// invalid in a barrier when their feature is off.
|
||||
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
// Family of `graphicsQueue`; the manager creates its own command pool
|
||||
// on it for the recycled upload-batch command buffers, so their parked
|
||||
// allocations never sit in (and fragment) the renderer's shared pool
|
||||
// that frame command buffers churn through every frame.
|
||||
Uint32 graphicsQueueFamilyIndex = 0;
|
||||
};
|
||||
|
||||
struct TextureResource {
|
||||
@@ -188,16 +61,12 @@ public:
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
// May differ from the image format: sRGB images attach through their UNORM
|
||||
// twin while GL_FRAMEBUFFER_SRGB is disabled.
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const AttachmentViewKey& other) const {
|
||||
return mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
viewFormat == other.viewFormat;
|
||||
viewType == other.viewType;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,8 +77,6 @@ public:
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -230,35 +97,17 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
|
||||
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
|
||||
// through two GL names in one draw, the parent with the stencil aspect and the view with
|
||||
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
|
||||
// same image. Without these two fields those views would alias each other in the cache.
|
||||
struct SampledImageViewKey {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
|
||||
// same window but different swizzles are different views. Baked into the key because
|
||||
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
|
||||
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
|
||||
// version moves, nothing else would ever notice a swizzle change on a view.
|
||||
Uint32 componentSwizzle = 0;
|
||||
|
||||
Bool operator==(const SampledImageViewKey& other) const {
|
||||
return baseMipLevel == other.baseMipLevel &&
|
||||
levelCount == other.levelCount &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format &&
|
||||
aspect == other.aspect &&
|
||||
componentSwizzle == other.componentSwizzle;
|
||||
format == other.format;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,15 +115,10 @@ public:
|
||||
SizeT operator()(const SampledImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -313,25 +157,7 @@ public:
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
// Usage the live image was created with. STORAGE is only requested for textures that
|
||||
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
||||
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
||||
// and recreates the image, so the resolved usage has to be part of the compatibility
|
||||
// check that decides whether the existing image can be kept.
|
||||
VkImageUsageFlags usageFlags = 0;
|
||||
// True once this image was (re)resolved while the texture was already marked as an
|
||||
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
||||
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||
Bool storageUsageResolved = false;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
|
||||
// command referencing this image that was recorded into the CURRENT frame
|
||||
// command buffer. An image untouched by the open recording may have its
|
||||
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -339,19 +165,6 @@ public:
|
||||
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
||||
// busts the skip even if it failed to bump the content version.
|
||||
Uint32 syncedMipLevelCount = 0;
|
||||
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
|
||||
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
|
||||
// already-defined level changes its size or format and dirties no texel, so it moves the
|
||||
// shape version and nothing else. Without this in the early-out key the image, its views
|
||||
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
|
||||
Uint64 syncedShapeVersion = 0;
|
||||
// The handle-keyed arm's (P5f fm) early-out key: the applier record's server-owned
|
||||
// Serial at the last successful sync. 0 also means "this resource has only ever been
|
||||
// synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves
|
||||
// on every mutation, so the first handle sync of a fresh image always re-checks.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 syncedWireSerial = 0;
|
||||
#endif
|
||||
|
||||
TextureResource() = default;
|
||||
TextureResource(const TextureResource&) = delete;
|
||||
@@ -377,16 +190,9 @@ public:
|
||||
std::swap(this->viewType, that.viewType);
|
||||
std::swap(this->sampleCount, that.sampleCount);
|
||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||
std::swap(this->usageFlags, that.usageFlags);
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::swap(this->syncedWireSerial, that.syncedWireSerial);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
@@ -445,15 +251,9 @@ public:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
usageFlags = 0;
|
||||
storageUsageResolved = false;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
syncedShapeVersion = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
syncedWireSerial = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
~TextureResource() {
|
||||
@@ -464,128 +264,12 @@ public:
|
||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct SampledTextureSnapshot {
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Submits the accumulated texture-upload batch (one command buffer, one
|
||||
// vkQueueSubmit, one pooled fence) if any uploads are pending. MUST run
|
||||
// before any other vkQueueSubmit on the shared graphics queue whose
|
||||
// commands may consume an image the batch writes - the frame command
|
||||
// buffer submit (mid-frame flush, readback, Present) and the
|
||||
// preserve-on-recreate copy are the existing callers. No-op when the
|
||||
// batch is empty.
|
||||
void FlushPendingUploads();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Non-blocking idle proof for wire-object retirement. Independent texture
|
||||
// submissions are not represented by VulkanRenderer's submit counter.
|
||||
Bool WireUploadsAreIdle();
|
||||
#endif
|
||||
|
||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
|
||||
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
|
||||
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
|
||||
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
|
||||
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
|
||||
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
|
||||
// layout field, so a second resource aliasing the same image would desynchronise the moment
|
||||
// either of them transitioned it.
|
||||
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// The window a GL texture object opens onto its storage image. For a plain texture this is
|
||||
// the resource's own full extent; for a view it is the sub-range, format and aspect
|
||||
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
|
||||
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
|
||||
// the storage texture's own defaults.
|
||||
struct TextureViewWindow {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
|
||||
VK_COMPONENT_SWIZZLE_A};
|
||||
Bool isTextureView = false;
|
||||
};
|
||||
|
||||
// The four component swizzles packed into one value, for the sampled-view cache key.
|
||||
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
|
||||
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
|
||||
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
|
||||
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
|
||||
}
|
||||
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
|
||||
const TextureResource& resource) const;
|
||||
// Records what a GL texture view needs of the image it views, so the next sync of the
|
||||
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
|
||||
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
|
||||
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture);
|
||||
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
|
||||
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with.
|
||||
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const;
|
||||
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
|
||||
// image. Shared back end of every GL-texture-view sampled path.
|
||||
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource, const TextureViewWindow& window);
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport
|
||||
// the apply thread may not name the client's ITextureObject (rule E), so the clear / blit /
|
||||
// readback / mipmap verbs resolve their attachment textures from the wire handle the
|
||||
// framebuffer or mip record carried: shape from the applier's resource record
|
||||
// (MGPipeApplier().TextureResources), texels from the server's staged-texture store
|
||||
// (P5c tx), pending-upload bookkeeping consumed from the same record. Returns null - never
|
||||
// a frontend fallback - when the record or the shape does not resolve; the caller declines
|
||||
// loudly, which is the honest answer a missing record deserves.
|
||||
//
|
||||
// The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle -
|
||||
// a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen}
|
||||
// can never inherit its predecessor's image. Texture views resolve the storage owner from
|
||||
// Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm.
|
||||
Bool ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, Uint32 level,
|
||||
const IntVec3& extent, VkFormat& format, Vector<Uint8>& bytes);
|
||||
TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false,
|
||||
Bool requireStorage = false);
|
||||
// Map a texture/view-relative subresource to its live storage owner. layerCount, when
|
||||
// supplied, receives the number of accessible layers starting at the input layer.
|
||||
// viewFormat is UNDEFINED for an ordinary texture, or the outer view's format.
|
||||
// outsideWindow, when supplied, says WHY a null came back: true only when every record on
|
||||
// the way was live and consistent and the (level, layer) simply is not there - past the
|
||||
// owner's levels or layers, or past a view's own window. A dead record, a missing or stale
|
||||
// view CSO and an unresolvable view format leave it false.
|
||||
MG_Pipe::MGPipeHandle ResolveWireTextureStorage(MG_Pipe::MGPipeHandle handle, Uint32& level,
|
||||
Uint32& layer, VkFormat* viewFormat = nullptr, Uint32* layerCount = nullptr,
|
||||
Bool* outsideWindow = nullptr);
|
||||
// Grows the live image of a handle-keyed resource to `requiredMipLevels`, carrying the
|
||||
// existing levels' content across with an in-command-buffer copy. The caller
|
||||
// (GenerateMipmap's record arm) has flushed every pending submission first, so the old
|
||||
// image is GPU-idle and the copy may be recorded into `commandBuffer` directly; the old
|
||||
// image goes to the deferred-release ring. False when the chain cannot grow (the resource
|
||||
// stays as it was).
|
||||
Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels,
|
||||
VkCommandBuffer commandBuffer);
|
||||
// Records that GPU writes superseded the staged CPU snapshot of this level. Layer
|
||||
// ranges select cube faces; array and 3D levels share one staged level shadow.
|
||||
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
#endif
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||
@@ -601,76 +285,23 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
// recording; a resource whose stamp does not match was not referenced by
|
||||
// any command in the open recording, so its out-of-pass work may safely
|
||||
// execute ahead of the whole recording (in the pre command buffer).
|
||||
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
|
||||
void StampResourceRecordingUse(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
// Map-lookup variant for callers that only hold the GL texture object.
|
||||
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
||||
// would cost far more than the compression it wins back.
|
||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||
// The same ordering question for the other recreate-and-preserve trigger: true when this
|
||||
// texture's live image carries a shorter mip chain than a full one, so defining the missing
|
||||
// levels recreates it and copies the old contents forward.
|
||||
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
|
||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||
|
||||
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
|
||||
// anything for an image that carries both aspects. Defaulted so the call sites that have no
|
||||
// texture in hand keep the depth-aspect answer they have always given.
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||
//
|
||||
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||
// Nothing does, or can, do that per layer.
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
||||
Uint32 layerCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
@@ -700,9 +331,6 @@ public:
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
TextureResource &outResource);
|
||||
@@ -733,47 +361,18 @@ private:
|
||||
void DeferViewRelease(VkImageView view);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyDeferredReleases();
|
||||
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
||||
// upload whose fence has signaled (submission order = completion order on
|
||||
// the single queue, so the scan stops at the first still-pending entry).
|
||||
// waitAll blocks on every entry - Shutdown's drain.
|
||||
void ReclaimCompletedUploads(Bool waitAll = false);
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
SizeT PruneDeadTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The two halves of SyncTextureResourceByHandle: shape from the resource record's
|
||||
// descriptor (create / recreate / compatibility), then the pending-upload walk against the
|
||||
// server's staged store. UploadPendingWireLevels mutates the applier's record (consumed
|
||||
// entries are dropped), which is why the record is a non-const reference.
|
||||
Bool SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource,
|
||||
Bool requireStorage);
|
||||
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
|
||||
TextureResource& resource);
|
||||
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
|
||||
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
|
||||
// across map-touching calls.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireTextureResources;
|
||||
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
|
||||
#endif
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
// Dedicated pool for the recycled upload-batch command buffers (see
|
||||
// InitInfo::graphicsQueueFamilyIndex).
|
||||
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
|
||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||
Bool m_imageFormatListSupported = false;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
||||
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
||||
Uint32 m_gcFrameCounter = 0;
|
||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||
// textures already fully synced in the current draw (small N -> flat scan).
|
||||
Bool m_drawSyncScopeActive = false;
|
||||
@@ -786,105 +385,12 @@ private:
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
||||
// are resolved on every draw, so cache their resource pointers and skip the
|
||||
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
||||
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
||||
// every memo entry must match. SyncTexture still runs on memo hits, so
|
||||
// content/param freshness is unaffected. A dead-then-reused texture address
|
||||
// cannot false-hit: the new object carries a new lifetime id.
|
||||
struct SyncedTextureMemoEntry {
|
||||
const MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
Uint64 eraseEpoch = 0;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
||||
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
||||
Uint32 m_syncedTextureMemoNext = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
|
||||
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
||||
// remembered rather than probed once globally.
|
||||
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
|
||||
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
|
||||
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
|
||||
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
|
||||
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
|
||||
// feature almost none of them use. A SAME-format view - which is the common case, and the
|
||||
// Better Clouds case - needs no flag at all and therefore costs nothing.
|
||||
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
|
||||
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
|
||||
// that NO other format will ever be viewed, and building a view outside it is
|
||||
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
|
||||
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||
|
||||
// --- Batched upload machinery ---
|
||||
// Uploads within a frame are recorded into ONE shared command buffer and
|
||||
// submitted with ONE vkQueueSubmit at FlushPendingUploads (the renderer
|
||||
// flushes before every frame-command-buffer submit). Staging memory comes
|
||||
// from a pool of persistently-mapped, reusable blocks instead of a
|
||||
// vmaCreateBuffer per upload.
|
||||
struct UploadStagingBlock {
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
Uint8* mapped = nullptr; // persistently mapped for the block's lifetime
|
||||
VkDeviceSize capacity = 0;
|
||||
VkDeviceSize cursor = 0; // bump cursor while the block backs the open batch
|
||||
};
|
||||
// Opens the batch command buffer lazily (allocates/reuses + begins recording).
|
||||
VkCommandBuffer EnsureUploadBatchOpen();
|
||||
// Bump-allocates `size` staging bytes for the open batch, growing onto a
|
||||
// new/pooled block when the current one cannot fit. Returns the write
|
||||
// pointer; outBuffer/outBaseOffset locate the space for copy commands.
|
||||
Uint8* AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer, VkDeviceSize& outBaseOffset);
|
||||
void RecycleUploadStagingBlock(UploadStagingBlock&& block);
|
||||
// Drops a recorded-but-unsubmitted batch on the floor. Shutdown only: the
|
||||
// device is being torn down, so the lost texel data is unobservable.
|
||||
void DiscardPendingUploadBatch();
|
||||
void DestroyUploadPools();
|
||||
|
||||
Vector<UploadStagingBlock> m_freeUploadStagingBlocks;
|
||||
VkDeviceSize m_freeUploadStagingBytes = 0;
|
||||
Vector<VkCommandBuffer> m_freeUploadCommandBuffers;
|
||||
Vector<VkFence> m_freeUploadFences;
|
||||
Bool m_uploadBatchOpen = false;
|
||||
VkCommandBuffer m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
|
||||
// Blocks whose staging bytes the open batch's copies reference (last =
|
||||
// the block the bump cursor is currently allocating from).
|
||||
Vector<UploadStagingBlock> m_uploadBatchBlocks;
|
||||
// Images the open batch writes; consulted for the rare re-upload-after-
|
||||
// draw flush and by DeferResourceRelease (an unsubmitted command buffer
|
||||
// referencing a deferred-released image would escape every fence-based
|
||||
// destruction proof, so the batch is flushed before the image is parked).
|
||||
Vector<VkImage> m_uploadBatchImages;
|
||||
VkDeviceSize m_uploadBatchStagingBytes = 0;
|
||||
|
||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||
// behind the queue serialized the CPU against the previous frame's GPU
|
||||
// work every time an animated atlas re-uploaded). Each flushed batch's
|
||||
// transients are parked here and RECYCLED (fence reset to the fence pool,
|
||||
// command buffer reset to the CB pool, staging blocks back to the block
|
||||
// pool) once the batch fence signals.
|
||||
struct PendingUploadReclaim {
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Vector<UploadStagingBlock> stagingBlocks;
|
||||
};
|
||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||
return false;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& poolState : m_pools) {
|
||||
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& poolState = m_pools[frameIndex];
|
||||
if (poolState.cursor >= m_slotsPerPool) {
|
||||
if (!poolState.exhaustionWarned) {
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
"this frame fall back to the frontend path",
|
||||
frameIndex, m_slotsPerPool);
|
||||
poolState.exhaustionWarned = true;
|
||||
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
||||
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
return false;
|
||||
}
|
||||
if (resultWithAvailability[1] == 0) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
#version 450
|
||||
layout(set = 0, binding = 0) uniform sampler2D sourceImage;
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
// Source framebuffer clipping preserves the original scale, rather than
|
||||
// extending the edge texels into pixels outside the source framebuffer.
|
||||
if (any(lessThan(texCoord, vec2(0.0))) || any(greaterThan(texCoord, vec2(1.0)))) discard;
|
||||
// Explicit LOD is required on Adreno 650: implicit LOD through a rotated
|
||||
// single-mip UBWC render target can read outside its allocation.
|
||||
outColor = textureLod(sourceImage, texCoord, 0.0);
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc.
|
||||
// The native blit owns no frontend object and never invokes the GL shader compiler.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireColorBlitSpirv.h"
|
||||
|
||||
struct VulkanRenderer::WireColorBlitResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireColorBlitResources() {
|
||||
if (!m_wireColorBlitResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
|
||||
// descriptor pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
vkDestroySampler(m_device, cache.nearest, nullptr);
|
||||
vkDestroySampler(m_device, cache.linear, nullptr);
|
||||
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.fragment, nullptr);
|
||||
delete m_wireColorBlitResources;
|
||||
m_wireColorBlitResources = nullptr;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BlitWireColorToDefault(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
|
||||
if (!destination.isDefault) return false;
|
||||
return BlitWireColorImage(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1, filter, false);
|
||||
}
|
||||
|
||||
// P7 wave 2-B (CONTRACT-P7 §3.2, `default-color-blit-shape`). The shader blit below can only
|
||||
// SAMPLE its source: sampler2D, a float result, and one of two 2D view types. A framebuffer
|
||||
// whose colour attachment is RGBA8UI, or a layer of a 3D texture, is none of those, and on a
|
||||
// rotated Android surface there is no other arm - an axis-aligned vkCmdBlitImage cannot
|
||||
// express a quarter turn. So the source region becomes an owned 2D image first, through
|
||||
// vkCmdCopyImage, which cares about the texel BLOCK SIZE and not about how the block is
|
||||
// interpreted; the scratch then takes the shader blit as an ordinary 2D float source.
|
||||
//
|
||||
// REINTERPRETATION IS THE POINT FOR THE INTEGER CASE, and GL agrees: glBlitFramebuffer
|
||||
// between an integer and a normalized format is INVALID_OPERATION (GL 4.6 core 18.3.1), so
|
||||
// there is no defined result to be wrong about - only a shape that used to end the session
|
||||
// and now produces the source's bits. For a float source of the wrong VIEW TYPE the copy is
|
||||
// a plain slice extraction and the result is exactly right.
|
||||
Bool VulkanRenderer::BlitWireColorToDefaultThroughScratch(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
|
||||
if (!destination.isDefault || source.isDefault) return false;
|
||||
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
|
||||
return false;
|
||||
if (!source.image || !destination.image || source.image == destination.image) return false;
|
||||
const Int64 width = std::abs(static_cast<Int64>(sx1) - sx0);
|
||||
const Int64 height = std::abs(static_cast<Int64>(sy1) - sy0);
|
||||
if (!width || !height || dx0 == dx1 || dy0 == dy1) return true;
|
||||
if (std::min(sx0, sx1) < 0 || std::min(sy0, sy1) < 0 ||
|
||||
static_cast<Uint64>(std::max(sx0, sx1)) > source.extent.width ||
|
||||
static_cast<Uint64>(std::max(sy0, sy1)) > source.extent.height) return false;
|
||||
// A compressed source cannot be a colour attachment, so it cannot be a framebuffer blit's
|
||||
// source either - but the block arithmetic below would silently produce nonsense for one,
|
||||
// and "cannot happen" is the reason the guards this package retires were written as
|
||||
// asserts. The size-compatibility classes are Vulkan's own (VkFormat texel block size);
|
||||
// the destination format is whatever the swapchain uses and is always sampled-float.
|
||||
if (vkuFormatIsCompressed(source.format)) return false;
|
||||
VkFormat scratchFormat = VK_FORMAT_UNDEFINED;
|
||||
switch (vkuFormatTexelBlockSize(source.format)) {
|
||||
case 1: scratchFormat = VK_FORMAT_R8_UNORM; break;
|
||||
case 2: scratchFormat = VK_FORMAT_R8G8_UNORM; break;
|
||||
case 4: scratchFormat = VK_FORMAT_R8G8B8A8_UNORM; break;
|
||||
case 8: scratchFormat = VK_FORMAT_R16G16B16A16_SFLOAT; break;
|
||||
case 16: scratchFormat = VK_FORMAT_R32G32B32A32_SFLOAT; break;
|
||||
default: return false;
|
||||
}
|
||||
VkFormatProperties scratchProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, scratchFormat, &scratchProperties);
|
||||
const VkFormatFeatureFlags scratchRequired = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
|
||||
if ((scratchProperties.optimalTilingFeatures & scratchRequired) != scratchRequired) return false;
|
||||
|
||||
auto scratch = MakeUnique<VkTextureManager::TextureResource>();
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = scratchFormat;
|
||||
info.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
info.mipLevels = info.arrayLayers = 1;
|
||||
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
VmaAllocationCreateInfo allocation{};
|
||||
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocation.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
if (vmaCreateImage(m_allocator, &info, &allocation, &scratch->image, &scratch->allocation,
|
||||
nullptr) != VK_SUCCESS)
|
||||
return false;
|
||||
scratch->format = scratchFormat;
|
||||
scratch->extent = {info.extent.width, info.extent.height};
|
||||
scratch->mipLevels = scratch->arrayLayers = scratch->depth = 1;
|
||||
scratch->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
WireImage staged{};
|
||||
staged.image = scratch->image;
|
||||
staged.format = scratchFormat;
|
||||
staged.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
staged.extent = scratch->extent;
|
||||
staged.trackedLayout = &scratch->layout;
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
TransitionWireImage(staged, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
VkImageCopy copy{};
|
||||
// A 3D source carries its slice on z and always names layer 0; every other source
|
||||
// carries it on the layer axis, which is the same split ResolveWireImage records.
|
||||
copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, source.level, source.is3D ? 0u : source.layer, 1};
|
||||
copy.srcOffset = {std::min(sx0, sx1), std::min(sy0, sy1),
|
||||
source.is3D ? static_cast<Int32>(source.layer) : 0};
|
||||
copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.dstOffset = {0, 0, 0};
|
||||
copy.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
vkCmdCopyImage(m_frameContext.GetCurrent().commandBuffer, source.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staged.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
staged.layout = scratch->layout;
|
||||
// The scratch holds the region at its own origin, so a reversed source rectangle
|
||||
// becomes a reversed rectangle OVER THE SCRATCH - the multisample resolve arm's shape.
|
||||
const GLint rx0 = sx1 > sx0 ? 0 : static_cast<GLint>(width);
|
||||
const GLint rx1 = sx1 > sx0 ? static_cast<GLint>(width) : 0;
|
||||
const GLint ry0 = sy1 > sy0 ? 0 : static_cast<GLint>(height);
|
||||
const GLint ry1 = sy1 > sy0 ? static_cast<GLint>(height) : 0;
|
||||
const Bool blitted = BlitWireColorImage(staged, destination, rx0, ry0, rx1, ry1,
|
||||
dx0, dy0, dx1, dy1, filter, false);
|
||||
// Retired whether or not the blit took: the copy above is already recorded against this
|
||||
// image, so the submission that consumes it owns the storage either way.
|
||||
WireRetiredObjects retired{};
|
||||
retired.textures.push_back(Move(scratch));
|
||||
RetireWireObjects(Move(retired));
|
||||
return blitted;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BlitWireColorImage(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap) {
|
||||
const Bool sameImage = source.image == destination.image;
|
||||
if (source.isDefault || (sameImage && source.level == destination.level && source.layer == destination.layer) ||
|
||||
source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT ||
|
||||
source.is3D || destination.is3D || (source.viewType != VK_IMAGE_VIEW_TYPE_2D &&
|
||||
source.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
|
||||
(destination.viewType != VK_IMAGE_VIEW_TYPE_2D && destination.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
|
||||
!vkuFormatIsSampledFloat(source.format)) return false;
|
||||
if (sx0 == sx1 || sy0 == sy1 || dx0 == dx1 || dy0 == dy1) return true;
|
||||
if (!source.extent.width || !source.extent.height ||
|
||||
!destination.extent.width || !destination.extent.height) return false;
|
||||
const auto transform = destination.isDefault ? m_swapchainObject.GetPreTransform() :
|
||||
VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) return false;
|
||||
VkFormatProperties sourceProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
|
||||
if ((sourceProperties.optimalTilingFeatures & required) != required) return false;
|
||||
// Mip generation samples and writes the texture's own color space;
|
||||
// GL_FRAMEBUFFER_SRGB only controls framebuffer operations.
|
||||
if (!mipmap) destination.format = ResolveSrgbAttachmentWriteFormat(destination.format,
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
VkFormatProperties destinationProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, &destinationProperties);
|
||||
if (!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) return false;
|
||||
const VkImageLayout sourceLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
const VkImageLayout destinationLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
|
||||
struct Constants {
|
||||
float srcRect[4];
|
||||
float dstRect[4];
|
||||
Int32 surfaceTransform;
|
||||
};
|
||||
static_assert(sizeof(Constants) == 36);
|
||||
if (!m_wireColorBlitResources) {
|
||||
m_wireColorBlitResources = new WireColorBlitResources;
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
|
||||
};
|
||||
shader(kWireColorBlitVertexSpirv, sizeof(kWireColorBlitVertexSpirv), cache.vertex);
|
||||
shader(kWireColorBlitFragmentSpirv, sizeof(kWireColorBlitFragmentSpirv), cache.fragment);
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(Constants)};
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
|
||||
pipelineLayout.pushConstantRangeCount = 1;
|
||||
pipelineLayout.pPushConstantRanges = &push;
|
||||
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_LINEAR;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.linear));
|
||||
}
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
WireColorBlitResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets) {
|
||||
if (entry.format == destination.format && entry.layout == destinationLayout) { target = &entry; break; }
|
||||
}
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = destination.format;
|
||||
target->layout = destinationLayout;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = destination.format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = attachment.finalLayout = destinationLayout;
|
||||
const VkAttachmentReference color{0, destinationLayout};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &color;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1;
|
||||
pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1;
|
||||
pass.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = cache.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = cache.fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipelineColorBlendAttachmentState colorMask{};
|
||||
colorMask.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
||||
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.attachmentCount = 1;
|
||||
blend.pAttachments = &colorMask;
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
pipeline.stageCount = 2;
|
||||
pipeline.pStages = stages;
|
||||
pipeline.pVertexInputState = &vertex;
|
||||
pipeline.pInputAssemblyState = &assembly;
|
||||
pipeline.pViewportState = &viewport;
|
||||
pipeline.pRasterizationState = &raster;
|
||||
pipeline.pMultisampleState = &samples;
|
||||
pipeline.pColorBlendState = &blend;
|
||||
pipeline.pDynamicState = &dynamic;
|
||||
pipeline.layout = cache.pipelineLayout;
|
||||
pipeline.renderPass = target->renderPass;
|
||||
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, &target->pipeline));
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
// Distinct mip/layer views of the same image are legal shader input and
|
||||
// output. GENERAL keeps the whole-image layout tracker truthful; the
|
||||
// transition also orders the previous mip's attachment write.
|
||||
TransitionWireImage(source, sourceLayout);
|
||||
if (sameImage) destination.layout = source.layout;
|
||||
else TransitionWireImage(destination, destinationLayout);
|
||||
const auto makeView = [&](const WireImage& image) {
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image;
|
||||
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
view.format = image.format;
|
||||
// A layered framebuffer blit reads its first layer, as the native path does.
|
||||
view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, image.level, 1, image.layer, 1};
|
||||
VkImageView handle = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
|
||||
return handle;
|
||||
};
|
||||
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &destinationView;
|
||||
framebufferInfo.width = destination.extent.width;
|
||||
framebufferInfo.height = destination.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
// Each in-flight blit owns its descriptor set. Updating a cached set
|
||||
// would rewrite the source view of an older queued blit.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
// GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the
|
||||
// four transforms) is a copy and samples NEAREST, which is exact where linear filtering
|
||||
// at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still
|
||||
// asks for SAMPLED_IMAGE_FILTER_LINEAR whenever GL said GL_LINEAR, so which blits reach
|
||||
// this pass is unchanged.
|
||||
const VkFilter samplerFilter = WireColorBlitFilterFor({sx0, sy0, sx1, sy1}, {dx0, dy0, dx1, dy1},
|
||||
filter, transform);
|
||||
const VkDescriptorImageInfo sampled{samplerFilter == VK_FILTER_LINEAR ? cache.linear : cache.nearest,
|
||||
sourceView, sourceLayout};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
const float sw = static_cast<float>(source.extent.width), sh = static_cast<float>(source.extent.height);
|
||||
const Bool quarterTurn = transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
|
||||
const float dw = static_cast<float>(quarterTurn ? destination.extent.height : destination.extent.width);
|
||||
const float dh = static_cast<float>(quarterTurn ? destination.extent.width : destination.extent.height);
|
||||
// The shared vertex shader flips window Y. Offscreen images keep the
|
||||
// source row order, so undo that flip in the destination rectangle.
|
||||
if (!destination.isDefault) {
|
||||
dy0 = static_cast<GLint>(destination.extent.height) - dy0;
|
||||
dy1 = static_cast<GLint>(destination.extent.height) - dy1;
|
||||
}
|
||||
const Constants constants{{sx0 / sw, sy0 / sh, (static_cast<float>(sx1) - sx0) / sw,
|
||||
(static_cast<float>(sy1) - sy0) / sh},
|
||||
{dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw,
|
||||
(static_cast<float>(dy1) - dy0) / dh},
|
||||
static_cast<Int32>(ToBlitSurfaceTransform(transform))};
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
static_cast<float>(destination.extent.height), 0.0f, 1.0f};
|
||||
const VkRect2D scissor{{0, 0}, destination.extent};
|
||||
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
|
||||
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
||||
vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, sizeof(constants), &constants);
|
||||
vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0);
|
||||
vkCmdEndRenderPass(frame.commandBuffer);
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the
|
||||
// application's descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, destinationView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan blit: no GL program, registry entry, or runtime transpilation.
|
||||
layout(push_constant) uniform BlitRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
} rect;
|
||||
layout(location = 0) out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
// A quad, rather than an oversized triangle, also clips partial blits to
|
||||
// their destination rectangle. The Vulkan viewport covers the native image.
|
||||
const vec2 corners[6] = vec2[](
|
||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
||||
vec2 uv = corners[gl_VertexIndex];
|
||||
vec2 p = (rect.dstRect.xy + uv * rect.dstRect.zw) * 2.0 - 1.0;
|
||||
// Same GL-bottom-left to native-image mapping as the monolithic blit.
|
||||
p.y = -p.y;
|
||||
if (rect.surfaceTransform == 1) p = vec2(-p.y, p.x);
|
||||
else if (rect.surfaceTransform == 2) p = -p;
|
||||
else if (rect.surfaceTransform == 3) p = vec2(p.y, -p.x);
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
texCoord = rect.srcRect.xy + uv * rect.srcRect.zw;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlitFilter.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
// P7 g3-blit (g3det, gate 3): WHICH SAMPLER THE WIRE ARM'S SHADER COLOUR BLIT READS ITS SOURCE
|
||||
// THROUGH.
|
||||
//
|
||||
// GL applies a blit's `filter` only where the image is STRETCHED (GL 4.6 core 18.3.1). A blit whose
|
||||
// source and destination rectangles are the same size is a copy, whatever `filter` says, and so is
|
||||
// a mirrored one. The wire arm's shader blit (WireColorBlit.inc, BlitWireColorImage) draws a quad
|
||||
// and samples `textureLod(sourceImage, texCoord, 0)` at interpolated coordinates, and it used the
|
||||
// LINEAR sampler for every GL_LINEAR blit. At 1:1 every fragment centre maps to a texel centre, so
|
||||
// in exact arithmetic linear filtering reads the same single texel. Adreno 830 quantises the
|
||||
// coordinate to sub-texel precision: a fragment that lands a fraction off the centre blends in a
|
||||
// small weight of a neighbour. minecraft-1.21.1-neoforge-create-instancing-in-world ends with a
|
||||
// 1:1 GL_LINEAR glBlitFramebuffer into FB 0, and on the wire arm 712 high-contrast pixels moved by
|
||||
// 1 LSB toward a neighbour. The monolith's vkCmdBlitImage copied them exactly
|
||||
// (~/w7/logs/g3det/VERDICT.md section 3). NEAREST within half a texel of the centre is exact, so an
|
||||
// unscaled blit takes NEAREST. Lavapipe samples texel centres exactly, so no host picture can see
|
||||
// the difference; this pure half is what the unit test pins
|
||||
// (MG_Test/Pipeline/PipelineQuirkTest.cpp, WireColorBlitSamples*).
|
||||
//
|
||||
// THE SURFACE TRANSFORM DOES NOT TURN A COPY INTO A STRETCH. BlitWireColorImage applies the
|
||||
// quarter and half turns in NDC, over a viewport of the native image, and for a quarter turn it
|
||||
// takes the logical extent as the native extent swapped. A native pixel then has exactly the pitch
|
||||
// of a logical one, and the rotation maps pixel centres onto pixel centres. So the scale test is on
|
||||
// GL's own (logical) rectangles for identity and all three rotations. BlitWireColorImage declines
|
||||
// any other transform before it chooses a sampler; for one, this answers with the filter's own
|
||||
// mapping, as before this package.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A glBlitFramebuffer rectangle as GL names it: two corners, either order.
|
||||
struct WireBlitRect {
|
||||
GLint x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||
};
|
||||
|
||||
// Same extent on both axes, orientation aside. The extents are taken in 64 bits: GL accepts any
|
||||
// GLint corner, and INT_MAX - INT_MIN does not fit in one.
|
||||
inline Bool WireBlitIsUnscaled(const WireBlitRect& source, const WireBlitRect& destination) {
|
||||
const auto extent = [](GLint a, GLint b) {
|
||||
return std::abs(static_cast<Int64>(b) - static_cast<Int64>(a));
|
||||
};
|
||||
return extent(source.x0, source.x1) == extent(destination.x0, destination.x1) &&
|
||||
extent(source.y0, source.y1) == extent(destination.y0, destination.y1);
|
||||
}
|
||||
|
||||
inline VkFilter WireColorBlitFilterFor(const WireBlitRect& source, const WireBlitRect& destination,
|
||||
GLenum filter, VkSurfaceTransformFlagBitsKHR transform) {
|
||||
if (filter != GL_LINEAR) return VK_FILTER_NEAREST;
|
||||
const Bool expressed = transform == VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
|
||||
if (expressed && WireBlitIsUnscaled(source, destination)) return VK_FILTER_NEAREST;
|
||||
return VK_FILTER_LINEAR;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,132 +0,0 @@
|
||||
// Generated from WireColorBlit.vert / WireColorBlit.frag (Vulkan 1.0 / SPIR-V 1.0) by
|
||||
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the
|
||||
// IN-TREE glslang and splices the words it emits into the arrays below. Do not edit the words
|
||||
// by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles every
|
||||
// source in its table and reds on the first word that disagrees.
|
||||
//
|
||||
// These words were hand-run through glslangValidator before that gate existed, and the gate
|
||||
// reproduces them exactly - which is the one thing nobody could have known beforehand.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireColorBlitVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000070u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0008000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000016u, 0x0000005du, 0x00000066u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00030005u,
|
||||
0x00000009u, 0x00007675u, 0x00060005u, 0x00000016u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x00000019u, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030005u, 0x0000001cu, 0x00000070u,
|
||||
0x00050005u, 0x0000001eu, 0x74696c42u, 0x74636552u, 0x00000000u, 0x00050006u, 0x0000001eu, 0x00000000u,
|
||||
0x52637273u, 0x00746365u, 0x00050006u, 0x0000001eu, 0x00000001u, 0x52747364u, 0x00746365u, 0x00080006u,
|
||||
0x0000001eu, 0x00000002u, 0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u, 0x00040005u,
|
||||
0x00000020u, 0x74636572u, 0x00000000u, 0x00060005u, 0x0000005bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||
0x00000000u, 0x00060006u, 0x0000005bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u,
|
||||
0x0000005bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000005bu,
|
||||
0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000005bu, 0x00000003u,
|
||||
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000005du, 0x00000000u, 0x00050005u,
|
||||
0x00000066u, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000016u, 0x0000000bu, 0x0000002au,
|
||||
0x00030047u, 0x0000001eu, 0x00000002u, 0x00050048u, 0x0000001eu, 0x00000000u, 0x00000023u, 0x00000000u,
|
||||
0x00050048u, 0x0000001eu, 0x00000001u, 0x00000023u, 0x00000010u, 0x00050048u, 0x0000001eu, 0x00000002u,
|
||||
0x00000023u, 0x00000020u, 0x00030047u, 0x0000005bu, 0x00000002u, 0x00050048u, 0x0000005bu, 0x00000000u,
|
||||
0x0000000bu, 0x00000000u, 0x00050048u, 0x0000005bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u,
|
||||
0x0000005bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000005bu, 0x00000003u, 0x0000000bu,
|
||||
0x00000004u, 0x00040047u, 0x00000066u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u,
|
||||
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
|
||||
0x00000002u, 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x00040015u, 0x0000000au, 0x00000020u,
|
||||
0x00000000u, 0x0004002bu, 0x0000000au, 0x0000000bu, 0x00000006u, 0x0004001cu, 0x0000000cu, 0x00000007u,
|
||||
0x0000000bu, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x0005002cu, 0x00000007u, 0x0000000eu,
|
||||
0x0000000du, 0x0000000du, 0x0004002bu, 0x00000006u, 0x0000000fu, 0x3f800000u, 0x0005002cu, 0x00000007u,
|
||||
0x00000010u, 0x0000000fu, 0x0000000du, 0x0005002cu, 0x00000007u, 0x00000011u, 0x0000000du, 0x0000000fu,
|
||||
0x0005002cu, 0x00000007u, 0x00000012u, 0x0000000fu, 0x0000000fu, 0x0009002cu, 0x0000000cu, 0x00000013u,
|
||||
0x0000000eu, 0x00000010u, 0x00000011u, 0x00000011u, 0x00000010u, 0x00000012u, 0x00040015u, 0x00000014u,
|
||||
0x00000020u, 0x00000001u, 0x00040020u, 0x00000015u, 0x00000001u, 0x00000014u, 0x0004003bu, 0x00000015u,
|
||||
0x00000016u, 0x00000001u, 0x00040020u, 0x00000018u, 0x00000007u, 0x0000000cu, 0x00040017u, 0x0000001du,
|
||||
0x00000006u, 0x00000004u, 0x0005001eu, 0x0000001eu, 0x0000001du, 0x0000001du, 0x00000014u, 0x00040020u,
|
||||
0x0000001fu, 0x00000009u, 0x0000001eu, 0x0004003bu, 0x0000001fu, 0x00000020u, 0x00000009u, 0x0004002bu,
|
||||
0x00000014u, 0x00000021u, 0x00000001u, 0x00040020u, 0x00000022u, 0x00000009u, 0x0000001du, 0x0004002bu,
|
||||
0x00000006u, 0x0000002cu, 0x40000000u, 0x0004002bu, 0x0000000au, 0x00000030u, 0x00000001u, 0x00040020u,
|
||||
0x00000031u, 0x00000007u, 0x00000006u, 0x0004002bu, 0x00000014u, 0x00000036u, 0x00000002u, 0x00040020u,
|
||||
0x00000037u, 0x00000009u, 0x00000014u, 0x00020014u, 0x0000003au, 0x0004002bu, 0x0000000au, 0x00000041u,
|
||||
0x00000000u, 0x0004002bu, 0x00000014u, 0x00000050u, 0x00000003u, 0x0004001cu, 0x0000005au, 0x00000006u,
|
||||
0x00000030u, 0x0006001eu, 0x0000005bu, 0x0000001du, 0x00000006u, 0x0000005au, 0x0000005au, 0x00040020u,
|
||||
0x0000005cu, 0x00000003u, 0x0000005bu, 0x0004003bu, 0x0000005cu, 0x0000005du, 0x00000003u, 0x0004002bu,
|
||||
0x00000014u, 0x0000005eu, 0x00000000u, 0x00040020u, 0x00000063u, 0x00000003u, 0x0000001du, 0x00040020u,
|
||||
0x00000065u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000065u, 0x00000066u, 0x00000003u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
|
||||
0x00000009u, 0x00000007u, 0x0004003bu, 0x00000018u, 0x00000019u, 0x00000007u, 0x0004003bu, 0x00000008u,
|
||||
0x0000001cu, 0x00000007u, 0x0004003du, 0x00000014u, 0x00000017u, 0x00000016u, 0x0003003eu, 0x00000019u,
|
||||
0x00000013u, 0x00050041u, 0x00000008u, 0x0000001au, 0x00000019u, 0x00000017u, 0x0004003du, 0x00000007u,
|
||||
0x0000001bu, 0x0000001au, 0x0003003eu, 0x00000009u, 0x0000001bu, 0x00050041u, 0x00000022u, 0x00000023u,
|
||||
0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du, 0x00000024u, 0x00000023u, 0x0007004fu, 0x00000007u,
|
||||
0x00000025u, 0x00000024u, 0x00000024u, 0x00000000u, 0x00000001u, 0x0004003du, 0x00000007u, 0x00000026u,
|
||||
0x00000009u, 0x00050041u, 0x00000022u, 0x00000027u, 0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du,
|
||||
0x00000028u, 0x00000027u, 0x0007004fu, 0x00000007u, 0x00000029u, 0x00000028u, 0x00000028u, 0x00000002u,
|
||||
0x00000003u, 0x00050085u, 0x00000007u, 0x0000002au, 0x00000026u, 0x00000029u, 0x00050081u, 0x00000007u,
|
||||
0x0000002bu, 0x00000025u, 0x0000002au, 0x0005008eu, 0x00000007u, 0x0000002du, 0x0000002bu, 0x0000002cu,
|
||||
0x00050050u, 0x00000007u, 0x0000002eu, 0x0000000fu, 0x0000000fu, 0x00050083u, 0x00000007u, 0x0000002fu,
|
||||
0x0000002du, 0x0000002eu, 0x0003003eu, 0x0000001cu, 0x0000002fu, 0x00050041u, 0x00000031u, 0x00000032u,
|
||||
0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000033u, 0x00000032u, 0x0004007fu, 0x00000006u,
|
||||
0x00000034u, 0x00000033u, 0x00050041u, 0x00000031u, 0x00000035u, 0x0000001cu, 0x00000030u, 0x0003003eu,
|
||||
0x00000035u, 0x00000034u, 0x00050041u, 0x00000037u, 0x00000038u, 0x00000020u, 0x00000036u, 0x0004003du,
|
||||
0x00000014u, 0x00000039u, 0x00000038u, 0x000500aau, 0x0000003au, 0x0000003bu, 0x00000039u, 0x00000021u,
|
||||
0x000300f7u, 0x0000003du, 0x00000000u, 0x000400fau, 0x0000003bu, 0x0000003cu, 0x00000045u, 0x000200f8u,
|
||||
0x0000003cu, 0x00050041u, 0x00000031u, 0x0000003eu, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u,
|
||||
0x0000003fu, 0x0000003eu, 0x0004007fu, 0x00000006u, 0x00000040u, 0x0000003fu, 0x00050041u, 0x00000031u,
|
||||
0x00000042u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u, 0x00000043u, 0x00000042u, 0x00050050u,
|
||||
0x00000007u, 0x00000044u, 0x00000040u, 0x00000043u, 0x0003003eu, 0x0000001cu, 0x00000044u, 0x000200f9u,
|
||||
0x0000003du, 0x000200f8u, 0x00000045u, 0x00050041u, 0x00000037u, 0x00000046u, 0x00000020u, 0x00000036u,
|
||||
0x0004003du, 0x00000014u, 0x00000047u, 0x00000046u, 0x000500aau, 0x0000003au, 0x00000048u, 0x00000047u,
|
||||
0x00000036u, 0x000300f7u, 0x0000004au, 0x00000000u, 0x000400fau, 0x00000048u, 0x00000049u, 0x0000004du,
|
||||
0x000200f8u, 0x00000049u, 0x0004003du, 0x00000007u, 0x0000004bu, 0x0000001cu, 0x0004007fu, 0x00000007u,
|
||||
0x0000004cu, 0x0000004bu, 0x0003003eu, 0x0000001cu, 0x0000004cu, 0x000200f9u, 0x0000004au, 0x000200f8u,
|
||||
0x0000004du, 0x00050041u, 0x00000037u, 0x0000004eu, 0x00000020u, 0x00000036u, 0x0004003du, 0x00000014u,
|
||||
0x0000004fu, 0x0000004eu, 0x000500aau, 0x0000003au, 0x00000051u, 0x0000004fu, 0x00000050u, 0x000300f7u,
|
||||
0x00000053u, 0x00000000u, 0x000400fau, 0x00000051u, 0x00000052u, 0x00000053u, 0x000200f8u, 0x00000052u,
|
||||
0x00050041u, 0x00000031u, 0x00000054u, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000055u,
|
||||
0x00000054u, 0x00050041u, 0x00000031u, 0x00000056u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u,
|
||||
0x00000057u, 0x00000056u, 0x0004007fu, 0x00000006u, 0x00000058u, 0x00000057u, 0x00050050u, 0x00000007u,
|
||||
0x00000059u, 0x00000055u, 0x00000058u, 0x0003003eu, 0x0000001cu, 0x00000059u, 0x000200f9u, 0x00000053u,
|
||||
0x000200f8u, 0x00000053u, 0x000200f9u, 0x0000004au, 0x000200f8u, 0x0000004au, 0x000200f9u, 0x0000003du,
|
||||
0x000200f8u, 0x0000003du, 0x0004003du, 0x00000007u, 0x0000005fu, 0x0000001cu, 0x00050051u, 0x00000006u,
|
||||
0x00000060u, 0x0000005fu, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000061u, 0x0000005fu, 0x00000001u,
|
||||
0x00070050u, 0x0000001du, 0x00000062u, 0x00000060u, 0x00000061u, 0x0000000du, 0x0000000fu, 0x00050041u,
|
||||
0x00000063u, 0x00000064u, 0x0000005du, 0x0000005eu, 0x0003003eu, 0x00000064u, 0x00000062u, 0x00050041u,
|
||||
0x00000022u, 0x00000067u, 0x00000020u, 0x0000005eu, 0x0004003du, 0x0000001du, 0x00000068u, 0x00000067u,
|
||||
0x0007004fu, 0x00000007u, 0x00000069u, 0x00000068u, 0x00000068u, 0x00000000u, 0x00000001u, 0x0004003du,
|
||||
0x00000007u, 0x0000006au, 0x00000009u, 0x00050041u, 0x00000022u, 0x0000006bu, 0x00000020u, 0x0000005eu,
|
||||
0x0004003du, 0x0000001du, 0x0000006cu, 0x0000006bu, 0x0007004fu, 0x00000007u, 0x0000006du, 0x0000006cu,
|
||||
0x0000006cu, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000006eu, 0x0000006au, 0x0000006du,
|
||||
0x00050081u, 0x00000007u, 0x0000006fu, 0x00000069u, 0x0000006eu, 0x0003003eu, 0x00000066u, 0x0000006fu,
|
||||
0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireColorBlitFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000027u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000au, 0x0000001fu, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
|
||||
0x00000000u, 0x00050005u, 0x0000000au, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00050005u, 0x0000001fu,
|
||||
0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00050005u, 0x00000023u, 0x72756f73u, 0x6d496563u, 0x00656761u,
|
||||
0x00040047u, 0x0000000au, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000001fu, 0x0000001eu, 0x00000000u,
|
||||
0x00040047u, 0x00000023u, 0x00000021u, 0x00000000u, 0x00040047u, 0x00000023u, 0x00000022u, 0x00000000u,
|
||||
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00020014u, 0x00000006u, 0x00030016u,
|
||||
0x00000007u, 0x00000020u, 0x00040017u, 0x00000008u, 0x00000007u, 0x00000002u, 0x00040020u, 0x00000009u,
|
||||
0x00000001u, 0x00000008u, 0x0004003bu, 0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000007u,
|
||||
0x0000000cu, 0x00000000u, 0x0005002cu, 0x00000008u, 0x0000000du, 0x0000000cu, 0x0000000cu, 0x00040017u,
|
||||
0x0000000eu, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000007u, 0x00000015u, 0x3f800000u, 0x0005002cu,
|
||||
0x00000008u, 0x00000016u, 0x00000015u, 0x00000015u, 0x00040017u, 0x0000001du, 0x00000007u, 0x00000004u,
|
||||
0x00040020u, 0x0000001eu, 0x00000003u, 0x0000001du, 0x0004003bu, 0x0000001eu, 0x0000001fu, 0x00000003u,
|
||||
0x00090019u, 0x00000020u, 0x00000007u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u,
|
||||
0x00000000u, 0x0003001bu, 0x00000021u, 0x00000020u, 0x00040020u, 0x00000022u, 0x00000000u, 0x00000021u,
|
||||
0x0004003bu, 0x00000022u, 0x00000023u, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000008u, 0x0000000bu, 0x0000000au, 0x000500b8u,
|
||||
0x0000000eu, 0x0000000fu, 0x0000000bu, 0x0000000du, 0x0004009au, 0x00000006u, 0x00000010u, 0x0000000fu,
|
||||
0x000400a8u, 0x00000006u, 0x00000011u, 0x00000010u, 0x000300f7u, 0x00000013u, 0x00000000u, 0x000400fau,
|
||||
0x00000011u, 0x00000012u, 0x00000013u, 0x000200f8u, 0x00000012u, 0x0004003du, 0x00000008u, 0x00000014u,
|
||||
0x0000000au, 0x000500bau, 0x0000000eu, 0x00000017u, 0x00000014u, 0x00000016u, 0x0004009au, 0x00000006u,
|
||||
0x00000018u, 0x00000017u, 0x000200f9u, 0x00000013u, 0x000200f8u, 0x00000013u, 0x000700f5u, 0x00000006u,
|
||||
0x00000019u, 0x00000010u, 0x00000005u, 0x00000018u, 0x00000012u, 0x000300f7u, 0x0000001bu, 0x00000000u,
|
||||
0x000400fau, 0x00000019u, 0x0000001au, 0x0000001bu, 0x000200f8u, 0x0000001au, 0x000100fcu, 0x000200f8u,
|
||||
0x0000001bu, 0x0004003du, 0x00000021u, 0x00000024u, 0x00000023u, 0x0004003du, 0x00000008u, 0x00000025u,
|
||||
0x0000000au, 0x00070058u, 0x0000001du, 0x00000026u, 0x00000024u, 0x00000025u, 0x00000002u, 0x0000000cu,
|
||||
0x0003003eu, 0x0000001fu, 0x00000026u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
@@ -1,172 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.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 <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// P7 wave 2 package B3: the per-site tally behind CONTRACT-P7 §0 rule I.
|
||||
//
|
||||
// Shape is PipeApplier.h's ServerVerbSink tallies, and for the same reason (R-16: a probe may
|
||||
// not arm against a stub). "The retrace passed" and "the retrace silently dropped nine draws"
|
||||
// produce the same exit code and the same log, so the lane has to assert the number that only
|
||||
// this path can move.
|
||||
//
|
||||
// Counters are process-wide and monotone. On the spawn arm they live in the SERVER process,
|
||||
// which is where the draw path runs - a client-side reader would always see zero, so the
|
||||
// two-process case asserts pixels and reads these from the server's own dump.
|
||||
enum class WireDeclineSite : Uint32 {
|
||||
#define MGL_WIRE_DECLINE(name) name,
|
||||
#include "WireDeclines.def"
|
||||
#undef MGL_WIRE_DECLINE
|
||||
Count
|
||||
};
|
||||
|
||||
inline const char* WireDeclineSiteName(WireDeclineSite site) {
|
||||
switch (site) {
|
||||
#define MGL_WIRE_DECLINE(name) \
|
||||
case WireDeclineSite::name: \
|
||||
return #name;
|
||||
#include "WireDeclines.def"
|
||||
#undef MGL_WIRE_DECLINE
|
||||
case WireDeclineSite::Count:
|
||||
break;
|
||||
}
|
||||
return "Unnamed";
|
||||
}
|
||||
|
||||
class WireDeclineTally {
|
||||
public:
|
||||
static constexpr SizeT kSiteCount = static_cast<SizeT>(WireDeclineSite::Count);
|
||||
|
||||
static void Count(WireDeclineSite site) {
|
||||
Slots()[static_cast<SizeT>(site)].fetch_add(1, std::memory_order_relaxed);
|
||||
Total().fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static Uint64 Get(WireDeclineSite site) {
|
||||
return Slots()[static_cast<SizeT>(site)].load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static Uint64 TotalDeclines() { return Total().load(std::memory_order_relaxed); }
|
||||
|
||||
// The memory half (package M1's thread): a refused upload does not consume its
|
||||
// PendingUpload entry, so the entry and the level shadow behind it stay alive. These are
|
||||
// GAUGES, not counters - the last observed live depth, sampled where the refusal happens -
|
||||
// so a monotone climb across frames is the signature that a refusal is also a leak.
|
||||
static void SetPendingGauge(Uint64 entries, Uint64 bytes) {
|
||||
PendingEntries().store(entries, std::memory_order_relaxed);
|
||||
PendingBytes().store(bytes, std::memory_order_relaxed);
|
||||
Uint64 peak = PeakPendingBytes().load(std::memory_order_relaxed);
|
||||
while (bytes > peak && !PeakPendingBytes().compare_exchange_weak(peak, bytes, std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
// The frame-serial floor's own counter. An "unsound serial complete" is an advance of
|
||||
// VkBufferManager's completed-frame-serial floor to a serial that ANOTHER submission still
|
||||
// in flight also carries - i.e. the renderer telling the buffer manager "frame N-1 is done"
|
||||
// while the submission holding frame N-1's buffer copies is still executing. It is what
|
||||
// lets a streamed glBufferSubData take the unordered host path and tear one draw's vertex
|
||||
// range. With a provable floor it is unreachable, so this counter is the red-once's reading:
|
||||
// non-zero on the wire arms before the fix, zero after, zero on monolith either way.
|
||||
static void CountUnsoundSerialComplete() {
|
||||
UnsoundSerial().fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
static Uint64 UnsoundSerialCompleteEvents() { return UnsoundSerial().load(std::memory_order_relaxed); }
|
||||
|
||||
static Uint64 PendingUploadEntries() { return PendingEntries().load(std::memory_order_relaxed); }
|
||||
static Uint64 PendingUploadBytes() { return PendingBytes().load(std::memory_order_relaxed); }
|
||||
static Uint64 PeakPendingUploadBytes() { return PeakPendingBytes().load(std::memory_order_relaxed); }
|
||||
|
||||
static void Reset() {
|
||||
for (SizeT i = 0; i < kSiteCount; ++i) Slots()[i].store(0, std::memory_order_relaxed);
|
||||
Total().store(0, std::memory_order_relaxed);
|
||||
UnsoundSerial().store(0, std::memory_order_relaxed);
|
||||
PendingEntries().store(0, std::memory_order_relaxed);
|
||||
PendingBytes().store(0, std::memory_order_relaxed);
|
||||
PeakPendingBytes().store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// One line per site that moved, plus the pending gauges. Called at session teardown and
|
||||
// from the forced-condition harness; silent when nothing declined, so a green lane stays
|
||||
// quiet and a lane that dropped a draw cannot.
|
||||
static void Dump(const char* tag) {
|
||||
// The floor counter prints unconditionally: "zero unsound advances" is the reading the
|
||||
// red-once needs, and an absent line cannot be told from a lane that never ran.
|
||||
MGLOG_I("MGWIRE-FLOOR[%s] unsoundSerialComplete=%llu", tag,
|
||||
static_cast<unsigned long long>(UnsoundSerialCompleteEvents()));
|
||||
if (TotalDeclines() == 0 && PeakPendingUploadBytes() == 0) return;
|
||||
MGLOG_I("MGWIRE-DECLINES[%s] total=%llu pendingEntries=%llu pendingBytes=%llu peakPendingBytes=%llu", tag,
|
||||
static_cast<unsigned long long>(TotalDeclines()),
|
||||
static_cast<unsigned long long>(PendingUploadEntries()),
|
||||
static_cast<unsigned long long>(PendingUploadBytes()),
|
||||
static_cast<unsigned long long>(PeakPendingUploadBytes()));
|
||||
for (SizeT i = 0; i < kSiteCount; ++i) {
|
||||
const Uint64 value = Slots()[i].load(std::memory_order_relaxed);
|
||||
if (!value) continue;
|
||||
MGLOG_I("MGWIRE-DECLINES[%s] %s=%llu", tag, WireDeclineSiteName(static_cast<WireDeclineSite>(i)),
|
||||
static_cast<unsigned long long>(value));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static std::atomic<Uint64>* Slots() {
|
||||
static std::atomic<Uint64> slots[kSiteCount]{};
|
||||
return slots;
|
||||
}
|
||||
static std::atomic<Uint64>& Total() {
|
||||
static std::atomic<Uint64> total{0};
|
||||
return total;
|
||||
}
|
||||
static std::atomic<Uint64>& PendingEntries() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& PendingBytes() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& PeakPendingBytes() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& UnsoundSerial() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Every decline goes through this: the tally moves ALWAYS, the log line is once per site.
|
||||
// `MGL_WIRE_DECLINE_AT(Site, "why", ...); return false;` is the statement pair that replaces
|
||||
// a bare `return false;` (the few sites that call WireDeclineTally::Count directly stand
|
||||
// under an MGLOG_W/E of their own; scripts/ci/wire_declines_audit.py holds them to that).
|
||||
//
|
||||
// A DECLINE IS NOT ALWAYS A SKIPPED DRAW, so the line does not say "draw". The Tex*, Shape* and
|
||||
// Upload* rows are counted per texture SYNC: SyncTextureResourceByHandle is reached from the
|
||||
// draw/dispatch texture preparation, but also from the descriptor resolver, framebuffer
|
||||
// attachments, CopyTexSubImage, mipmap generation and CopyImageSubData - each of which Fatals
|
||||
// right after a failed sync - and from texture readback, which falls back to the unbacked
|
||||
// shadow. A draw that samples three unsyncable textures counts three; a failed attachment
|
||||
// sync counts once and is then a Fatal, not a skipped draw.
|
||||
#define MGL_WIRE_DECLINE_AT(site, fmt, ...) \
|
||||
do { \
|
||||
::MobileGL::MG_Backend::DirectVulkan::WireDeclineTally::Count( \
|
||||
::MobileGL::MG_Backend::DirectVulkan::WireDeclineSite::site); \
|
||||
MGLOG_E_ONCE("Magma wire decline [" #site "]: " fmt, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -1,106 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.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
|
||||
|
||||
// P7 wave 2 package B3. THE NAMES OF EVERY WAY THE MAGMA WIRE ARM CAN DECLINE.
|
||||
//
|
||||
// A DECLINE IS NOT ALWAYS A SKIPPED DRAW. The SetupWireDraw / DispatchWireCompute rows are one
|
||||
// per refused draw or dispatch; the Tex* / Shape* / Upload* rows are counted per texture SYNC,
|
||||
// which the draw path reaches once per sampled texture and the descriptor resolver, framebuffer
|
||||
// attachments, CopyTexSubImage, mipmap generation, CopyImageSubData and texture readback reach
|
||||
// on their own (WireDeclineTally.h, above MGL_WIRE_DECLINE_AT).
|
||||
//
|
||||
// CONTRACT-P7 §0 rule I says a refusal on the wire arm is either a named decline or a named
|
||||
// Fatal. Before B3 the draw path had a third shape the rule does not admit: a bare
|
||||
// `return false` with no log, no counter and no census entry - roughly twenty of them in
|
||||
// SetupWireDraw alone, plus the texture-sync chain behind PrepareWireTextureResources. A draw
|
||||
// that took one of those exits left NO trace anywhere, which is precisely why the OpenRA
|
||||
// device divergence survived four device windows with client and server logs that were
|
||||
// LINE-IDENTICAL between a passing and a failing run.
|
||||
//
|
||||
// Each row below is one such exit. The name is what the decline logs and what the tally
|
||||
// counts, so "which exit did this frame take" is answerable from a counter dump instead of
|
||||
// from a bisect. Rows are append-only: the integration lane asserts specific counters moved.
|
||||
//
|
||||
// EVERY ROW HAS A SITE AND EVERY SITE HAS A LINE. A row with no site is a name that can never
|
||||
// be counted - it reads as "this cannot happen here" when it only means "nobody wired it up" -
|
||||
// and a site with no line puts the reader back on a bisect. Sites reached through a macro
|
||||
// (MGL_WIRE_DECLINE_AT) get both at once; the few that use WireDeclineTally::Count directly do
|
||||
// so ONLY because the statement above them already carries the same reason as an _ONCE log.
|
||||
// scripts/ci/wire_declines_audit.py holds both halves.
|
||||
//
|
||||
// MGL_WIRE_DECLINE(Name)
|
||||
|
||||
// ---- SetupWireDraw, in statement order -------------------------------------------------
|
||||
MGL_WIRE_DECLINE(ProgramLinkOrSpirv)
|
||||
MGL_WIRE_DECLINE(PointSizeCapability)
|
||||
MGL_WIRE_DECLINE(TextureResourcesUnprepared)
|
||||
MGL_WIRE_DECLINE(VertexFormatUnsupported)
|
||||
MGL_WIRE_DECLINE(VertexAttributeWindow)
|
||||
MGL_WIRE_DECLINE(VertexStreamCountZero)
|
||||
MGL_WIRE_DECLINE(VertexStreamRead)
|
||||
MGL_WIRE_DECLINE(VertexStreamConverter)
|
||||
MGL_WIRE_DECLINE(VertexTransientUpload)
|
||||
MGL_WIRE_DECLINE(IndexViewMissing)
|
||||
MGL_WIRE_DECLINE(IndexTypeWidth)
|
||||
MGL_WIRE_DECLINE(IndexWindow)
|
||||
MGL_WIRE_DECLINE(IndexStreamRead)
|
||||
MGL_WIRE_DECLINE(IndexTransientUpload)
|
||||
MGL_WIRE_DECLINE(IndexSliceInvalid)
|
||||
MGL_WIRE_DECLINE(UniformBufferBinding)
|
||||
MGL_WIRE_DECLINE(NoAttachments)
|
||||
MGL_WIRE_DECLINE(PipelineUnavailable)
|
||||
MGL_WIRE_DECLINE(CurrentAttributePayload)
|
||||
MGL_WIRE_DECLINE(CurrentAttributeUpload)
|
||||
MGL_WIRE_DECLINE(BeginRenderPass)
|
||||
|
||||
// ---- DispatchWireCompute ----------------------------------------------------------------
|
||||
MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv)
|
||||
MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared)
|
||||
MGL_WIRE_DECLINE(ComputePipelineUnavailable)
|
||||
MGL_WIRE_DECLINE(ComputeUniformBufferBinding)
|
||||
|
||||
// ---- UniformManager::PrepareWireTextureResources ----------------------------------------
|
||||
MGL_WIRE_DECLINE(DescriptorDeclined)
|
||||
MGL_WIRE_DECLINE(SamplerLocationAlias)
|
||||
MGL_WIRE_DECLINE(SamplerUnitRange)
|
||||
|
||||
// ---- VkTextureManager::SyncTextureResourceByHandle --------------------------------------
|
||||
MGL_WIRE_DECLINE(TexResolveStorage)
|
||||
MGL_WIRE_DECLINE(TexViewFormat)
|
||||
MGL_WIRE_DECLINE(TexNoApplierRecord)
|
||||
MGL_WIRE_DECLINE(TexRecordDeadOrStale)
|
||||
MGL_WIRE_DECLINE(TexShapeSync)
|
||||
MGL_WIRE_DECLINE(TexUploadPendingLevels)
|
||||
MGL_WIRE_DECLINE(TexStorageUsage)
|
||||
|
||||
// ---- VkTextureManager::UploadPendingWireLevels ------------------------------------------
|
||||
// The five `incomplete`/false exits the investigation named, plus the two that leave an
|
||||
// entry pending WITHOUT failing (a `continue`) - those are the ones that can starve a draw
|
||||
// of its texels while still reporting success, so they are counted separately.
|
||||
MGL_WIRE_DECLINE(UploadLevelBeyondChain)
|
||||
MGL_WIRE_DECLINE(UploadLevelExtentMismatch)
|
||||
MGL_WIRE_DECLINE(UploadEmptyLevel)
|
||||
MGL_WIRE_DECLINE(UploadConversion)
|
||||
MGL_WIRE_DECLINE(UploadTexelArithmetic)
|
||||
MGL_WIRE_DECLINE(UploadRegionOutOfBounds)
|
||||
MGL_WIRE_DECLINE(UploadIncomplete)
|
||||
MGL_WIRE_DECLINE(UploadFlushFailed)
|
||||
|
||||
// ---- VkTextureManager::SyncWireTextureShape ---------------------------------------------
|
||||
MGL_WIRE_DECLINE(ShapeBufferBacked)
|
||||
MGL_WIRE_DECLINE(ShapeViewOfStorage)
|
||||
MGL_WIRE_DECLINE(ShapeNoLevels)
|
||||
MGL_WIRE_DECLINE(ShapeNoVulkanShape)
|
||||
MGL_WIRE_DECLINE(ShapeNoVkFormat)
|
||||
MGL_WIRE_DECLINE(ShapeSampleCount)
|
||||
MGL_WIRE_DECLINE(ShapePreserveFlushFailed)
|
||||
MGL_WIRE_DECLINE(ShapeImageFlagsUnsupported)
|
||||
MGL_WIRE_DECLINE(ShapeCreateImageFailed)
|
||||
// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in
|
||||
// SyncWireTextureShape still leaving no trace at all.
|
||||
MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
|
||||
@@ -1,28 +0,0 @@
|
||||
#version 450
|
||||
// The monolith arm's kDepthMipmapFragmentShaderSource (VulkanRenderer.cpp), ported to the
|
||||
// baked pipeline: the same 2x2 box of texelFetches, the same clamp, the same average, the
|
||||
// same gl_FragDepth. It is a PORT and not a rewrite on purpose - CONTRACT-P7 §3.2 retires a
|
||||
// refusal by making the wire arm observably the monolith's, so the two arms have to agree
|
||||
// texel for texel, including where they read.
|
||||
//
|
||||
// uSrcTexelSize arrives on the push constant rather than in a uniform block: the wire arm
|
||||
// owns no GL program, so there is no default-block uniform to write and no global UBO to
|
||||
// bind. That is the whole of OQ-13's narrow half for this program.
|
||||
layout(push_constant) uniform DepthMipmapRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
ivec2 srcTexelSize;
|
||||
} rect;
|
||||
layout(set = 0, binding = 0) uniform sampler2D sourceImage;
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
ivec2 srcBase = ivec2(texCoord * vec2(rect.srcTexelSize));
|
||||
ivec2 srcMax = rect.srcTexelSize - ivec2(1);
|
||||
float depth0 = texelFetch(sourceImage, clamp(srcBase, ivec2(0), srcMax), 0).r;
|
||||
float depth1 = texelFetch(sourceImage, clamp(srcBase + ivec2(1, 0), ivec2(0), srcMax), 0).r;
|
||||
float depth2 = texelFetch(sourceImage, clamp(srcBase + ivec2(0, 1), ivec2(0), srcMax), 0).r;
|
||||
float depth3 = texelFetch(sourceImage, clamp(srcBase + ivec2(1, 1), ivec2(0), srcMax), 0).r;
|
||||
gl_FragDepth = 0.25 * (depth0 + depth1 + depth2 + depth3);
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireColorBlit.inc.
|
||||
// The native depth mip owns no frontend object and never invokes the GL shader compiler.
|
||||
//
|
||||
// WHY THIS FILE EXISTS AT ALL. vkCmdBlitImage is the ordinary way to halve a mip, and for a
|
||||
// depth format it is often not available: VK_FORMAT_FEATURE_BLIT_DST is optional for depth,
|
||||
// and the monolith arm has carried a shader fallback for exactly that case since before the
|
||||
// split (VulkanRenderer.cpp's GenerateDepthMipmapWithShader). The wire arm had no such
|
||||
// fallback, so every depth mip the device could not blit died as
|
||||
// `Magma:mipmap-shader-format-or-shape`, and a DEPTH|STENCIL texture died one guard earlier
|
||||
// as `Magma:depth-stencil-mipmap@P7` whether or not it could be blitted.
|
||||
//
|
||||
// The monolith fallback is not reusable here and that is the whole of CONTRACT-P7 §5.1 (A):
|
||||
// it builds a GL ProgramObject, two GL ShaderObjects and a GL sampler, binds through the
|
||||
// uniform manager's global UBO, and writes uSrcTexelSize as a default-block uniform - four
|
||||
// frontend objects and a GL compiler on the server's apply thread. The baked program below is
|
||||
// the same shader with the same arithmetic, driven by a push constant and its own descriptor
|
||||
// set, which is what lets the server generate a depth mip while owning nothing of the client.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireDepthMipmapSpirv.h"
|
||||
|
||||
struct VulkanRenderer::WireDepthMipmapResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireDepthMipmapResources() {
|
||||
if (!m_wireDepthMipmapResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
|
||||
// descriptor pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
vkDestroySampler(m_device, cache.nearest, nullptr);
|
||||
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.fragment, nullptr);
|
||||
delete m_wireDepthMipmapResources;
|
||||
m_wireDepthMipmapResources = nullptr;
|
||||
}
|
||||
|
||||
// ONE LEVEL, source -> destination, both depth subresources of the same image. The shape
|
||||
// mirrors BlitWireColorImage deliberately: the same lazily built module pair, the same
|
||||
// per-format render pass/pipeline cache, the same per-submit descriptor pool, and the
|
||||
// same GENERAL-on-both-sides layout rule for a same-image pass, so the two native passes
|
||||
// cannot disagree about how an image is handed between them.
|
||||
Bool VulkanRenderer::GenerateWireDepthMipLevel(WireImage source, WireImage destination) {
|
||||
const Bool sameImage = source.image == destination.image;
|
||||
if (!source.image || !destination.image) return false;
|
||||
if (sameImage && source.level == destination.level && source.layer == destination.layer)
|
||||
return false;
|
||||
if (source.isDefault || destination.isDefault) return false;
|
||||
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
|
||||
return false;
|
||||
if (source.is3D || destination.is3D) return false;
|
||||
if (source.format != destination.format) return false;
|
||||
// DEPTH ONLY. A combined depth/stencil image can be sampled for its depth and
|
||||
// rendered as a depth attachment, but this pass writes gl_FragDepth and NOTHING
|
||||
// else - the stencil aspect would come out of the render pass undefined, which is
|
||||
// not what GL's glGenerateMipmap promises for the level it just produced. The
|
||||
// caller declines that shape rather than producing half a level.
|
||||
if (source.aspect != VK_IMAGE_ASPECT_DEPTH_BIT ||
|
||||
destination.aspect != VK_IMAGE_ASPECT_DEPTH_BIT) return false;
|
||||
if (!source.extent.width || !source.extent.height ||
|
||||
!destination.extent.width || !destination.extent.height) return false;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
if ((properties.optimalTilingFeatures & required) != required) return false;
|
||||
|
||||
// The baked block's layout, and the offsets are not a guess: the generated header
|
||||
// decorates its members at 0 / 16 / 32 / 40, which is where a vec4, a vec4, an int
|
||||
// and an 8-byte-aligned ivec2 land. The pad is what makes the C++ agree.
|
||||
struct Constants {
|
||||
float srcRect[4];
|
||||
float dstRect[4];
|
||||
Int32 surfaceTransform;
|
||||
Int32 reservedForIvec2Alignment;
|
||||
Int32 srcTexelSize[2];
|
||||
};
|
||||
static_assert(sizeof(Constants) == 48);
|
||||
static_assert(offsetof(Constants, srcTexelSize) == 40);
|
||||
|
||||
if (!m_wireDepthMipmapResources) {
|
||||
m_wireDepthMipmapResources = new WireDepthMipmapResources;
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
|
||||
};
|
||||
shader(kWireDepthMipmapVertexSpirv, sizeof(kWireDepthMipmapVertexSpirv), cache.vertex);
|
||||
shader(kWireDepthMipmapFragmentSpirv, sizeof(kWireDepthMipmapFragmentSpirv), cache.fragment);
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
|
||||
// BOTH STAGES ON ONE RANGE: the block is declared identically in the vertex and
|
||||
// fragment sources (a push-constant block must be), the vertex stage reads the
|
||||
// rects and the fragment stage reads srcTexelSize.
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(Constants)};
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
|
||||
pipelineLayout.pushConstantRangeCount = 1;
|
||||
pipelineLayout.pPushConstantRanges = &push;
|
||||
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
|
||||
// NEAREST, and it is not a default: the fragment shader does its own 2x2 box with
|
||||
// texelFetch, so any filtering the sampler added would be applied twice. It also
|
||||
// keeps the sampler legal on a device that does not advertise linear filtering for
|
||||
// depth formats, which is most of them.
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW =
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
|
||||
}
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
|
||||
const VkImageLayout sourceLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
const VkImageLayout destinationLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL
|
||||
: VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
WireDepthMipmapResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets) {
|
||||
if (entry.format == destination.format && entry.layout == destinationLayout) {
|
||||
target = &entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = destination.format;
|
||||
target->layout = destinationLayout;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = destination.format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// DONT_CARE, not LOAD: the pass writes gl_FragDepth at every texel of the level
|
||||
// it is producing, so there is nothing in the destination worth loading, and a
|
||||
// level that has just been grown has no defined contents to load anyway.
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = attachment.finalLayout = destinationLayout;
|
||||
const VkAttachmentReference depth{0, destinationLayout};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &depth;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1;
|
||||
pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1;
|
||||
pass.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = cache.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = cache.fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// ALWAYS + write enabled: the fragment shader's gl_FragDepth is the answer, and a
|
||||
// comparison against whatever DONT_CARE left behind would discard some of it.
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = VK_TRUE;
|
||||
depthStencil.depthWriteEnable = VK_TRUE;
|
||||
depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||
depthStencil.minDepthBounds = 0.0f;
|
||||
depthStencil.maxDepthBounds = 1.0f;
|
||||
// No colour attachment in this render pass, so no attachment states either.
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
pipeline.stageCount = 2;
|
||||
pipeline.pStages = stages;
|
||||
pipeline.pVertexInputState = &vertex;
|
||||
pipeline.pInputAssemblyState = &assembly;
|
||||
pipeline.pViewportState = &viewport;
|
||||
pipeline.pRasterizationState = &raster;
|
||||
pipeline.pMultisampleState = &samples;
|
||||
pipeline.pDepthStencilState = &depthStencil;
|
||||
pipeline.pColorBlendState = &blend;
|
||||
pipeline.pDynamicState = &dynamic;
|
||||
pipeline.layout = cache.pipelineLayout;
|
||||
pipeline.renderPass = target->renderPass;
|
||||
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, &target->pipeline));
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
// Distinct mip views of the same image are legal shader input and output at once.
|
||||
// GENERAL keeps the whole-image layout tracker truthful; the transition also orders
|
||||
// the previous level's attachment write against this level's sample of it.
|
||||
TransitionWireImage(source, sourceLayout);
|
||||
if (sameImage) destination.layout = source.layout;
|
||||
else TransitionWireImage(destination, destinationLayout);
|
||||
const auto makeView = [&](const WireImage& image) {
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image;
|
||||
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
view.format = image.format;
|
||||
view.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, image.level, 1, image.layer, 1};
|
||||
VkImageView handle = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
|
||||
return handle;
|
||||
};
|
||||
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &destinationView;
|
||||
framebufferInfo.width = destination.extent.width;
|
||||
framebufferInfo.height = destination.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
// Each in-flight pass owns its descriptor set, for the colour blit's reason: updating
|
||||
// a cached set would rewrite the source view of an older queued pass.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
const VkDescriptorImageInfo sampled{cache.nearest, sourceView, sourceLayout};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
// The destination rectangle is flipped exactly as BlitWireColorImage flips it for an
|
||||
// offscreen target, because it shares that file's vertex shader and therefore its
|
||||
// window-Y flip. With the flip undone, destination row r samples texCoord.y
|
||||
// (r + 0.5) / dstHeight, and the fragment's ivec2(texCoord * srcTexelSize) lands on
|
||||
// the same source texels the monolith arm's fullscreen triangle lands on - which is
|
||||
// the point: §3.2 retires a refusal by making the wire arm observably the monolith's,
|
||||
// half-texel offset included.
|
||||
const Constants constants{
|
||||
{0.0f, 0.0f, 1.0f, 1.0f},
|
||||
{0.0f, 1.0f, 1.0f, -1.0f},
|
||||
0, 0,
|
||||
{static_cast<Int32>(source.extent.width), static_cast<Int32>(source.extent.height)}};
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
static_cast<float>(destination.extent.height), 0.0f, 1.0f};
|
||||
const VkRect2D scissor{{0, 0}, destination.extent};
|
||||
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
|
||||
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
||||
vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(constants), &constants);
|
||||
vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0);
|
||||
vkCmdEndRenderPass(frame.commandBuffer);
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the application's
|
||||
// descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, destinationView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,33 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan depth mip generation: no GL program, registry entry, or runtime transpilation.
|
||||
//
|
||||
// The geometry is WireColorBlit.vert's, unchanged - the same quad, the same push-constant
|
||||
// rect, the same GL-bottom-left to native-image mapping - so a depth mip and a colour blit
|
||||
// place their fragments identically and only the fragment stage differs. The block carries
|
||||
// one member more than the colour blit's, and the fragment stage is the member's only
|
||||
// reader: a push-constant block must be declared identically in every stage of one pipeline,
|
||||
// so it is declared here too rather than split into a second range.
|
||||
layout(push_constant) uniform DepthMipmapRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
ivec2 srcTexelSize;
|
||||
} rect;
|
||||
layout(location = 0) out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
// A quad, rather than an oversized triangle, also clips partial blits to
|
||||
// their destination rectangle. The Vulkan viewport covers the native image.
|
||||
const vec2 corners[6] = vec2[](
|
||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
||||
vec2 uv = corners[gl_VertexIndex];
|
||||
vec2 p = (rect.dstRect.xy + uv * rect.dstRect.zw) * 2.0 - 1.0;
|
||||
// Same GL-bottom-left to native-image mapping as the monolithic blit.
|
||||
p.y = -p.y;
|
||||
if (rect.surfaceTransform == 1) p = vec2(-p.y, p.x);
|
||||
else if (rect.surfaceTransform == 2) p = -p;
|
||||
else if (rect.surfaceTransform == 3) p = vec2(p.y, -p.x);
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
texCoord = rect.srcRect.xy + uv * rect.srcRect.zw;
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Generated from WireDepthMipmap.vert / WireDepthMipmap.frag by
|
||||
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the
|
||||
// IN-TREE glslang and splices the words it emits into the arrays below. Do not edit the
|
||||
// words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles
|
||||
// every source in its table and reds on the first word that disagrees.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireDepthMipmapVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000071u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0008000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000016u, 0x0000005eu, 0x00000067u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00030005u,
|
||||
0x00000009u, 0x00007675u, 0x00060005u, 0x00000016u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x00000019u, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030005u, 0x0000001cu, 0x00000070u,
|
||||
0x00060005u, 0x0000001fu, 0x74706544u, 0x70694d68u, 0x5270616du, 0x00746365u, 0x00050006u, 0x0000001fu,
|
||||
0x00000000u, 0x52637273u, 0x00746365u, 0x00050006u, 0x0000001fu, 0x00000001u, 0x52747364u, 0x00746365u,
|
||||
0x00080006u, 0x0000001fu, 0x00000002u, 0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u,
|
||||
0x00070006u, 0x0000001fu, 0x00000003u, 0x54637273u, 0x6c657865u, 0x657a6953u, 0x00000000u, 0x00040005u,
|
||||
0x00000021u, 0x74636572u, 0x00000000u, 0x00060005u, 0x0000005cu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||
0x00000000u, 0x00060006u, 0x0000005cu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u,
|
||||
0x0000005cu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000005cu,
|
||||
0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000005cu, 0x00000003u,
|
||||
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000005eu, 0x00000000u, 0x00050005u,
|
||||
0x00000067u, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000016u, 0x0000000bu, 0x0000002au,
|
||||
0x00030047u, 0x0000001fu, 0x00000002u, 0x00050048u, 0x0000001fu, 0x00000000u, 0x00000023u, 0x00000000u,
|
||||
0x00050048u, 0x0000001fu, 0x00000001u, 0x00000023u, 0x00000010u, 0x00050048u, 0x0000001fu, 0x00000002u,
|
||||
0x00000023u, 0x00000020u, 0x00050048u, 0x0000001fu, 0x00000003u, 0x00000023u, 0x00000028u, 0x00030047u,
|
||||
0x0000005cu, 0x00000002u, 0x00050048u, 0x0000005cu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000005cu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000005cu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000005cu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000067u,
|
||||
0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u,
|
||||
0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000002u, 0x00040020u, 0x00000008u,
|
||||
0x00000007u, 0x00000007u, 0x00040015u, 0x0000000au, 0x00000020u, 0x00000000u, 0x0004002bu, 0x0000000au,
|
||||
0x0000000bu, 0x00000006u, 0x0004001cu, 0x0000000cu, 0x00000007u, 0x0000000bu, 0x0004002bu, 0x00000006u,
|
||||
0x0000000du, 0x00000000u, 0x0005002cu, 0x00000007u, 0x0000000eu, 0x0000000du, 0x0000000du, 0x0004002bu,
|
||||
0x00000006u, 0x0000000fu, 0x3f800000u, 0x0005002cu, 0x00000007u, 0x00000010u, 0x0000000fu, 0x0000000du,
|
||||
0x0005002cu, 0x00000007u, 0x00000011u, 0x0000000du, 0x0000000fu, 0x0005002cu, 0x00000007u, 0x00000012u,
|
||||
0x0000000fu, 0x0000000fu, 0x0009002cu, 0x0000000cu, 0x00000013u, 0x0000000eu, 0x00000010u, 0x00000011u,
|
||||
0x00000011u, 0x00000010u, 0x00000012u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040020u,
|
||||
0x00000015u, 0x00000001u, 0x00000014u, 0x0004003bu, 0x00000015u, 0x00000016u, 0x00000001u, 0x00040020u,
|
||||
0x00000018u, 0x00000007u, 0x0000000cu, 0x00040017u, 0x0000001du, 0x00000006u, 0x00000004u, 0x00040017u,
|
||||
0x0000001eu, 0x00000014u, 0x00000002u, 0x0006001eu, 0x0000001fu, 0x0000001du, 0x0000001du, 0x00000014u,
|
||||
0x0000001eu, 0x00040020u, 0x00000020u, 0x00000009u, 0x0000001fu, 0x0004003bu, 0x00000020u, 0x00000021u,
|
||||
0x00000009u, 0x0004002bu, 0x00000014u, 0x00000022u, 0x00000001u, 0x00040020u, 0x00000023u, 0x00000009u,
|
||||
0x0000001du, 0x0004002bu, 0x00000006u, 0x0000002du, 0x40000000u, 0x0004002bu, 0x0000000au, 0x00000031u,
|
||||
0x00000001u, 0x00040020u, 0x00000032u, 0x00000007u, 0x00000006u, 0x0004002bu, 0x00000014u, 0x00000037u,
|
||||
0x00000002u, 0x00040020u, 0x00000038u, 0x00000009u, 0x00000014u, 0x00020014u, 0x0000003bu, 0x0004002bu,
|
||||
0x0000000au, 0x00000042u, 0x00000000u, 0x0004002bu, 0x00000014u, 0x00000051u, 0x00000003u, 0x0004001cu,
|
||||
0x0000005bu, 0x00000006u, 0x00000031u, 0x0006001eu, 0x0000005cu, 0x0000001du, 0x00000006u, 0x0000005bu,
|
||||
0x0000005bu, 0x00040020u, 0x0000005du, 0x00000003u, 0x0000005cu, 0x0004003bu, 0x0000005du, 0x0000005eu,
|
||||
0x00000003u, 0x0004002bu, 0x00000014u, 0x0000005fu, 0x00000000u, 0x00040020u, 0x00000064u, 0x00000003u,
|
||||
0x0000001du, 0x00040020u, 0x00000066u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000066u, 0x00000067u,
|
||||
0x00000003u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u,
|
||||
0x0004003bu, 0x00000008u, 0x00000009u, 0x00000007u, 0x0004003bu, 0x00000018u, 0x00000019u, 0x00000007u,
|
||||
0x0004003bu, 0x00000008u, 0x0000001cu, 0x00000007u, 0x0004003du, 0x00000014u, 0x00000017u, 0x00000016u,
|
||||
0x0003003eu, 0x00000019u, 0x00000013u, 0x00050041u, 0x00000008u, 0x0000001au, 0x00000019u, 0x00000017u,
|
||||
0x0004003du, 0x00000007u, 0x0000001bu, 0x0000001au, 0x0003003eu, 0x00000009u, 0x0000001bu, 0x00050041u,
|
||||
0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x0004003du, 0x0000001du, 0x00000025u, 0x00000024u,
|
||||
0x0007004fu, 0x00000007u, 0x00000026u, 0x00000025u, 0x00000025u, 0x00000000u, 0x00000001u, 0x0004003du,
|
||||
0x00000007u, 0x00000027u, 0x00000009u, 0x00050041u, 0x00000023u, 0x00000028u, 0x00000021u, 0x00000022u,
|
||||
0x0004003du, 0x0000001du, 0x00000029u, 0x00000028u, 0x0007004fu, 0x00000007u, 0x0000002au, 0x00000029u,
|
||||
0x00000029u, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000002bu, 0x00000027u, 0x0000002au,
|
||||
0x00050081u, 0x00000007u, 0x0000002cu, 0x00000026u, 0x0000002bu, 0x0005008eu, 0x00000007u, 0x0000002eu,
|
||||
0x0000002cu, 0x0000002du, 0x00050050u, 0x00000007u, 0x0000002fu, 0x0000000fu, 0x0000000fu, 0x00050083u,
|
||||
0x00000007u, 0x00000030u, 0x0000002eu, 0x0000002fu, 0x0003003eu, 0x0000001cu, 0x00000030u, 0x00050041u,
|
||||
0x00000032u, 0x00000033u, 0x0000001cu, 0x00000031u, 0x0004003du, 0x00000006u, 0x00000034u, 0x00000033u,
|
||||
0x0004007fu, 0x00000006u, 0x00000035u, 0x00000034u, 0x00050041u, 0x00000032u, 0x00000036u, 0x0000001cu,
|
||||
0x00000031u, 0x0003003eu, 0x00000036u, 0x00000035u, 0x00050041u, 0x00000038u, 0x00000039u, 0x00000021u,
|
||||
0x00000037u, 0x0004003du, 0x00000014u, 0x0000003au, 0x00000039u, 0x000500aau, 0x0000003bu, 0x0000003cu,
|
||||
0x0000003au, 0x00000022u, 0x000300f7u, 0x0000003eu, 0x00000000u, 0x000400fau, 0x0000003cu, 0x0000003du,
|
||||
0x00000046u, 0x000200f8u, 0x0000003du, 0x00050041u, 0x00000032u, 0x0000003fu, 0x0000001cu, 0x00000031u,
|
||||
0x0004003du, 0x00000006u, 0x00000040u, 0x0000003fu, 0x0004007fu, 0x00000006u, 0x00000041u, 0x00000040u,
|
||||
0x00050041u, 0x00000032u, 0x00000043u, 0x0000001cu, 0x00000042u, 0x0004003du, 0x00000006u, 0x00000044u,
|
||||
0x00000043u, 0x00050050u, 0x00000007u, 0x00000045u, 0x00000041u, 0x00000044u, 0x0003003eu, 0x0000001cu,
|
||||
0x00000045u, 0x000200f9u, 0x0000003eu, 0x000200f8u, 0x00000046u, 0x00050041u, 0x00000038u, 0x00000047u,
|
||||
0x00000021u, 0x00000037u, 0x0004003du, 0x00000014u, 0x00000048u, 0x00000047u, 0x000500aau, 0x0000003bu,
|
||||
0x00000049u, 0x00000048u, 0x00000037u, 0x000300f7u, 0x0000004bu, 0x00000000u, 0x000400fau, 0x00000049u,
|
||||
0x0000004au, 0x0000004eu, 0x000200f8u, 0x0000004au, 0x0004003du, 0x00000007u, 0x0000004cu, 0x0000001cu,
|
||||
0x0004007fu, 0x00000007u, 0x0000004du, 0x0000004cu, 0x0003003eu, 0x0000001cu, 0x0000004du, 0x000200f9u,
|
||||
0x0000004bu, 0x000200f8u, 0x0000004eu, 0x00050041u, 0x00000038u, 0x0000004fu, 0x00000021u, 0x00000037u,
|
||||
0x0004003du, 0x00000014u, 0x00000050u, 0x0000004fu, 0x000500aau, 0x0000003bu, 0x00000052u, 0x00000050u,
|
||||
0x00000051u, 0x000300f7u, 0x00000054u, 0x00000000u, 0x000400fau, 0x00000052u, 0x00000053u, 0x00000054u,
|
||||
0x000200f8u, 0x00000053u, 0x00050041u, 0x00000032u, 0x00000055u, 0x0000001cu, 0x00000031u, 0x0004003du,
|
||||
0x00000006u, 0x00000056u, 0x00000055u, 0x00050041u, 0x00000032u, 0x00000057u, 0x0000001cu, 0x00000042u,
|
||||
0x0004003du, 0x00000006u, 0x00000058u, 0x00000057u, 0x0004007fu, 0x00000006u, 0x00000059u, 0x00000058u,
|
||||
0x00050050u, 0x00000007u, 0x0000005au, 0x00000056u, 0x00000059u, 0x0003003eu, 0x0000001cu, 0x0000005au,
|
||||
0x000200f9u, 0x00000054u, 0x000200f8u, 0x00000054u, 0x000200f9u, 0x0000004bu, 0x000200f8u, 0x0000004bu,
|
||||
0x000200f9u, 0x0000003eu, 0x000200f8u, 0x0000003eu, 0x0004003du, 0x00000007u, 0x00000060u, 0x0000001cu,
|
||||
0x00050051u, 0x00000006u, 0x00000061u, 0x00000060u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000062u,
|
||||
0x00000060u, 0x00000001u, 0x00070050u, 0x0000001du, 0x00000063u, 0x00000061u, 0x00000062u, 0x0000000du,
|
||||
0x0000000fu, 0x00050041u, 0x00000064u, 0x00000065u, 0x0000005eu, 0x0000005fu, 0x0003003eu, 0x00000065u,
|
||||
0x00000063u, 0x00050041u, 0x00000023u, 0x00000068u, 0x00000021u, 0x0000005fu, 0x0004003du, 0x0000001du,
|
||||
0x00000069u, 0x00000068u, 0x0007004fu, 0x00000007u, 0x0000006au, 0x00000069u, 0x00000069u, 0x00000000u,
|
||||
0x00000001u, 0x0004003du, 0x00000007u, 0x0000006bu, 0x00000009u, 0x00050041u, 0x00000023u, 0x0000006cu,
|
||||
0x00000021u, 0x0000005fu, 0x0004003du, 0x0000001du, 0x0000006du, 0x0000006cu, 0x0007004fu, 0x00000007u,
|
||||
0x0000006eu, 0x0000006du, 0x0000006du, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000006fu,
|
||||
0x0000006bu, 0x0000006eu, 0x00050081u, 0x00000007u, 0x00000070u, 0x0000006au, 0x0000006fu, 0x0003003eu,
|
||||
0x00000067u, 0x00000070u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireDepthMipmapFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000059u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000004fu, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u,
|
||||
0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00040005u, 0x00000009u, 0x42637273u, 0x00657361u,
|
||||
0x00050005u, 0x0000000du, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00060005u, 0x00000010u, 0x74706544u,
|
||||
0x70694d68u, 0x5270616du, 0x00746365u, 0x00050006u, 0x00000010u, 0x00000000u, 0x52637273u, 0x00746365u,
|
||||
0x00050006u, 0x00000010u, 0x00000001u, 0x52747364u, 0x00746365u, 0x00080006u, 0x00000010u, 0x00000002u,
|
||||
0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u, 0x00070006u, 0x00000010u, 0x00000003u,
|
||||
0x54637273u, 0x6c657865u, 0x657a6953u, 0x00000000u, 0x00040005u, 0x00000012u, 0x74636572u, 0x00000000u,
|
||||
0x00040005u, 0x0000001au, 0x4d637273u, 0x00007861u, 0x00040005u, 0x00000021u, 0x74706564u, 0x00003068u,
|
||||
0x00050005u, 0x00000025u, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00040005u, 0x00000031u, 0x74706564u,
|
||||
0x00003168u, 0x00040005u, 0x0000003bu, 0x74706564u, 0x00003268u, 0x00040005u, 0x00000045u, 0x74706564u,
|
||||
0x00003368u, 0x00060005u, 0x0000004fu, 0x465f6c67u, 0x44676172u, 0x68747065u, 0x00000000u, 0x00040047u,
|
||||
0x0000000du, 0x0000001eu, 0x00000000u, 0x00030047u, 0x00000010u, 0x00000002u, 0x00050048u, 0x00000010u,
|
||||
0x00000000u, 0x00000023u, 0x00000000u, 0x00050048u, 0x00000010u, 0x00000001u, 0x00000023u, 0x00000010u,
|
||||
0x00050048u, 0x00000010u, 0x00000002u, 0x00000023u, 0x00000020u, 0x00050048u, 0x00000010u, 0x00000003u,
|
||||
0x00000023u, 0x00000028u, 0x00040047u, 0x00000025u, 0x00000021u, 0x00000000u, 0x00040047u, 0x00000025u,
|
||||
0x00000022u, 0x00000000u, 0x00040047u, 0x0000004fu, 0x0000000bu, 0x00000016u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040017u,
|
||||
0x00000007u, 0x00000006u, 0x00000002u, 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x00030016u,
|
||||
0x0000000au, 0x00000020u, 0x00040017u, 0x0000000bu, 0x0000000au, 0x00000002u, 0x00040020u, 0x0000000cu,
|
||||
0x00000001u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000001u, 0x00040017u, 0x0000000fu,
|
||||
0x0000000au, 0x00000004u, 0x0006001eu, 0x00000010u, 0x0000000fu, 0x0000000fu, 0x00000006u, 0x00000007u,
|
||||
0x00040020u, 0x00000011u, 0x00000009u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000009u,
|
||||
0x0004002bu, 0x00000006u, 0x00000013u, 0x00000003u, 0x00040020u, 0x00000014u, 0x00000009u, 0x00000007u,
|
||||
0x0004002bu, 0x00000006u, 0x0000001du, 0x00000001u, 0x0005002cu, 0x00000007u, 0x0000001eu, 0x0000001du,
|
||||
0x0000001du, 0x00040020u, 0x00000020u, 0x00000007u, 0x0000000au, 0x00090019u, 0x00000022u, 0x0000000au,
|
||||
0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x00000023u,
|
||||
0x00000022u, 0x00040020u, 0x00000024u, 0x00000000u, 0x00000023u, 0x0004003bu, 0x00000024u, 0x00000025u,
|
||||
0x00000000u, 0x0004002bu, 0x00000006u, 0x00000028u, 0x00000000u, 0x0005002cu, 0x00000007u, 0x00000029u,
|
||||
0x00000028u, 0x00000028u, 0x00040015u, 0x0000002eu, 0x00000020u, 0x00000000u, 0x0004002bu, 0x0000002eu,
|
||||
0x0000002fu, 0x00000000u, 0x0005002cu, 0x00000007u, 0x00000034u, 0x0000001du, 0x00000028u, 0x0005002cu,
|
||||
0x00000007u, 0x0000003eu, 0x00000028u, 0x0000001du, 0x00040020u, 0x0000004eu, 0x00000003u, 0x0000000au,
|
||||
0x0004003bu, 0x0000004eu, 0x0000004fu, 0x00000003u, 0x0004002bu, 0x0000000au, 0x00000050u, 0x3e800000u,
|
||||
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu,
|
||||
0x00000008u, 0x00000009u, 0x00000007u, 0x0004003bu, 0x00000008u, 0x0000001au, 0x00000007u, 0x0004003bu,
|
||||
0x00000020u, 0x00000021u, 0x00000007u, 0x0004003bu, 0x00000020u, 0x00000031u, 0x00000007u, 0x0004003bu,
|
||||
0x00000020u, 0x0000003bu, 0x00000007u, 0x0004003bu, 0x00000020u, 0x00000045u, 0x00000007u, 0x0004003du,
|
||||
0x0000000bu, 0x0000000eu, 0x0000000du, 0x00050041u, 0x00000014u, 0x00000015u, 0x00000012u, 0x00000013u,
|
||||
0x0004003du, 0x00000007u, 0x00000016u, 0x00000015u, 0x0004006fu, 0x0000000bu, 0x00000017u, 0x00000016u,
|
||||
0x00050085u, 0x0000000bu, 0x00000018u, 0x0000000eu, 0x00000017u, 0x0004006eu, 0x00000007u, 0x00000019u,
|
||||
0x00000018u, 0x0003003eu, 0x00000009u, 0x00000019u, 0x00050041u, 0x00000014u, 0x0000001bu, 0x00000012u,
|
||||
0x00000013u, 0x0004003du, 0x00000007u, 0x0000001cu, 0x0000001bu, 0x00050082u, 0x00000007u, 0x0000001fu,
|
||||
0x0000001cu, 0x0000001eu, 0x0003003eu, 0x0000001au, 0x0000001fu, 0x0004003du, 0x00000023u, 0x00000026u,
|
||||
0x00000025u, 0x0004003du, 0x00000007u, 0x00000027u, 0x00000009u, 0x0004003du, 0x00000007u, 0x0000002au,
|
||||
0x0000001au, 0x0008000cu, 0x00000007u, 0x0000002bu, 0x00000001u, 0x0000002du, 0x00000027u, 0x00000029u,
|
||||
0x0000002au, 0x00040064u, 0x00000022u, 0x0000002cu, 0x00000026u, 0x0007005fu, 0x0000000fu, 0x0000002du,
|
||||
0x0000002cu, 0x0000002bu, 0x00000002u, 0x00000028u, 0x00050051u, 0x0000000au, 0x00000030u, 0x0000002du,
|
||||
0x00000000u, 0x0003003eu, 0x00000021u, 0x00000030u, 0x0004003du, 0x00000023u, 0x00000032u, 0x00000025u,
|
||||
0x0004003du, 0x00000007u, 0x00000033u, 0x00000009u, 0x00050080u, 0x00000007u, 0x00000035u, 0x00000033u,
|
||||
0x00000034u, 0x0004003du, 0x00000007u, 0x00000036u, 0x0000001au, 0x0008000cu, 0x00000007u, 0x00000037u,
|
||||
0x00000001u, 0x0000002du, 0x00000035u, 0x00000029u, 0x00000036u, 0x00040064u, 0x00000022u, 0x00000038u,
|
||||
0x00000032u, 0x0007005fu, 0x0000000fu, 0x00000039u, 0x00000038u, 0x00000037u, 0x00000002u, 0x00000028u,
|
||||
0x00050051u, 0x0000000au, 0x0000003au, 0x00000039u, 0x00000000u, 0x0003003eu, 0x00000031u, 0x0000003au,
|
||||
0x0004003du, 0x00000023u, 0x0000003cu, 0x00000025u, 0x0004003du, 0x00000007u, 0x0000003du, 0x00000009u,
|
||||
0x00050080u, 0x00000007u, 0x0000003fu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000007u, 0x00000040u,
|
||||
0x0000001au, 0x0008000cu, 0x00000007u, 0x00000041u, 0x00000001u, 0x0000002du, 0x0000003fu, 0x00000029u,
|
||||
0x00000040u, 0x00040064u, 0x00000022u, 0x00000042u, 0x0000003cu, 0x0007005fu, 0x0000000fu, 0x00000043u,
|
||||
0x00000042u, 0x00000041u, 0x00000002u, 0x00000028u, 0x00050051u, 0x0000000au, 0x00000044u, 0x00000043u,
|
||||
0x00000000u, 0x0003003eu, 0x0000003bu, 0x00000044u, 0x0004003du, 0x00000023u, 0x00000046u, 0x00000025u,
|
||||
0x0004003du, 0x00000007u, 0x00000047u, 0x00000009u, 0x00050080u, 0x00000007u, 0x00000048u, 0x00000047u,
|
||||
0x0000001eu, 0x0004003du, 0x00000007u, 0x00000049u, 0x0000001au, 0x0008000cu, 0x00000007u, 0x0000004au,
|
||||
0x00000001u, 0x0000002du, 0x00000048u, 0x00000029u, 0x00000049u, 0x00040064u, 0x00000022u, 0x0000004bu,
|
||||
0x00000046u, 0x0007005fu, 0x0000000fu, 0x0000004cu, 0x0000004bu, 0x0000004au, 0x00000002u, 0x00000028u,
|
||||
0x00050051u, 0x0000000au, 0x0000004du, 0x0000004cu, 0x00000000u, 0x0003003eu, 0x00000045u, 0x0000004du,
|
||||
0x0004003du, 0x0000000au, 0x00000051u, 0x00000021u, 0x0004003du, 0x0000000au, 0x00000052u, 0x00000031u,
|
||||
0x00050081u, 0x0000000au, 0x00000053u, 0x00000051u, 0x00000052u, 0x0004003du, 0x0000000au, 0x00000054u,
|
||||
0x0000003bu, 0x00050081u, 0x0000000au, 0x00000055u, 0x00000053u, 0x00000054u, 0x0004003du, 0x0000000au,
|
||||
0x00000056u, 0x00000045u, 0x00050081u, 0x0000000au, 0x00000057u, 0x00000055u, 0x00000056u, 0x00050085u,
|
||||
0x0000000au, 0x00000058u, 0x00000050u, 0x00000057u, 0x0003003eu, 0x0000004fu, 0x00000058u, 0x000100fdu,
|
||||
0x00010038u,
|
||||
};
|
||||
@@ -1,329 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
// P7 gate 5 (g5-msrbo, g5-msprobe): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST.
|
||||
//
|
||||
// The wire arm has two (WireFramebuffer.inc, ResolveWireDepthStencil): a render pass that carries
|
||||
// a VK_KHR_depth_stencil_resolve attachment and no draw, and the baked shader pass of
|
||||
// WireMultisampleResolve.inc, which draws. P7 wave 2-B2 took the render pass wherever the
|
||||
// extension exists. On the Redmi (Adreno 830, Vulkan 1.3.284, driver 512.800.71) that pass left
|
||||
// its resolve target unwritten - KHR-GL46.direct_state_access.renderbuffers_storage_multisample
|
||||
// read every resolved depth and stencil value back as 0 on the inproc arm while the monolith arm
|
||||
// passed, and the same case passed on the device with the shader arm forced
|
||||
// (MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1).
|
||||
//
|
||||
// A DRIVER THAT CLAIMS WHAT IT CANNOT DO IS A POST PROBE, NOT A VENDOR QUIRK (standing rule,
|
||||
// 2026-08-22). The first fix keyed the order on Qualcomm's vendor id; it is replaced by a probe
|
||||
// that reproduces the production resolve on the actual VkDevice (WireDepthResolveProbe.cpp) and
|
||||
// asks the one question the order depends on: does the no-draw render pass write the resolve
|
||||
// target? A driver that fixes the defect gets the fast arm back, and a driver of any vendor that
|
||||
// has it gets the shader arm - neither needs a table edit.
|
||||
//
|
||||
// EVERY BUG PROBE CARRIES A CONTROL (MG_Util/SelfTest/DriverBugProbes.h). The control is the
|
||||
// production SHADER arm fed the same inputs: when it does not resolve them either, the probe has
|
||||
// measured its own setup (or a device that cannot resolve depth at all) rather than the render
|
||||
// pass, the verdict is INCONCLUSIVE, and the default order stands.
|
||||
//
|
||||
// This header is the pure half - the measurement, the verdict, the test knob and the choice - so
|
||||
// a unit test pins every mapping with a fake result source (MG_Test/Pipeline/PipelineQuirkTest.cpp).
|
||||
// The Vulkan half that fills the measurement is WireDepthResolveProbe.{h,cpp}.
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// One aspect of one probed format, both arms. Values are the RAW aspect words the
|
||||
// buffer copy returns (D16: 16 bits; D24: the low 24 bits of the 32-bit word; D32F: the float's
|
||||
// bits; stencil: 8 bits), compared within `tolerance` (1 LSB for a UNORM depth, 0 otherwise).
|
||||
struct WireDepthResolveAspectReading {
|
||||
// The format has this aspect and the render-pass arm's copy of it was read back.
|
||||
Bool measured = false;
|
||||
Uint32 texels = 0;
|
||||
Uint32 expected = 0; // the multisample source's cleared value
|
||||
Uint32 sentinel = 0; // what the resolve target was pre-filled with
|
||||
Uint32 tolerance = 0;
|
||||
// THE SUBJECT: VK_KHR_depth_stencil_resolve render pass, no draw.
|
||||
Uint32 renderPassMatches = 0; // texels within tolerance of `expected`
|
||||
Uint32 renderPassSentinels = 0; // texels still holding the sentinel
|
||||
Uint32 renderPassFirst = 0; // texel (0,0)
|
||||
// THE CONTROL: the production shader pass, same inputs. False where it cannot run for this
|
||||
// aspect (a stencil aspect without VK_EXT_shader_stencil_export, a failed pipeline).
|
||||
Bool shaderRan = false;
|
||||
Uint32 shaderMatches = 0;
|
||||
Uint32 shaderFirst = 0;
|
||||
};
|
||||
|
||||
struct WireDepthResolveFormatReading {
|
||||
String name; // e.g. "D24_UNORM_S8_UINT"
|
||||
Int32 format = 0; // the VkFormat
|
||||
Uint32 samples = 0;
|
||||
// False when the format was not probed; `skipReason` says why (unsupported, the render-pass
|
||||
// arm would not be taken for it, an object the probe needed could not be created).
|
||||
Bool ran = false;
|
||||
String skipReason;
|
||||
WireDepthResolveAspectReading depth, stencil;
|
||||
};
|
||||
|
||||
struct WireDepthResolveProbeMeasurement {
|
||||
// At least one format was recorded, submitted and read back.
|
||||
Bool ran = false;
|
||||
// The bounded fence wait expired: every object the probe created was deliberately leaked
|
||||
// (the queue may still be executing it), and nothing was read back.
|
||||
Bool fenceWaitTimedOut = false;
|
||||
String failureReason;
|
||||
// Came from MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean, not from the device.
|
||||
Bool fromKnob = false;
|
||||
// Measured on the device, but with MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject: the probe
|
||||
// recorded no render-pass resolve at all, so the subject's target kept its sentinel.
|
||||
Bool subjectElided = false;
|
||||
Vector<WireDepthResolveFormatReading> formats;
|
||||
};
|
||||
|
||||
enum class WireDepthResolveProbeVerdict : Uint8 {
|
||||
// Nothing was measured: the render-pass arm is not available (nothing to choose between),
|
||||
// or the probe could not set itself up. The default order stands.
|
||||
NotRun,
|
||||
// Measured, but no format's CONTROL resolved: the probe cannot tell the render pass's
|
||||
// defect from its own. The default order stands, and the report must never call it a bug.
|
||||
Inconclusive,
|
||||
// Every format whose control resolved was resolved by the render pass too.
|
||||
Clean,
|
||||
// The defect: on at least one format the shader control resolved the inputs and the
|
||||
// render pass did not (wrote nothing - the sentinel survived - or wrote wrong values).
|
||||
RenderPassResolveBroken,
|
||||
};
|
||||
|
||||
inline const char* WireDepthResolveProbeVerdictName(WireDepthResolveProbeVerdict verdict) {
|
||||
switch (verdict) {
|
||||
case WireDepthResolveProbeVerdict::NotRun: return "not-run";
|
||||
case WireDepthResolveProbeVerdict::Inconclusive: return "inconclusive";
|
||||
case WireDepthResolveProbeVerdict::Clean: return "clean";
|
||||
case WireDepthResolveProbeVerdict::RenderPassResolveBroken: return "render-pass-resolve-broken";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
inline Bool WireDepthResolveControlHolds(const WireDepthResolveAspectReading& aspect) {
|
||||
return aspect.measured && aspect.shaderRan && aspect.texels > 0 && aspect.shaderMatches == aspect.texels;
|
||||
}
|
||||
|
||||
inline Bool WireDepthResolveSubjectHolds(const WireDepthResolveAspectReading& aspect) {
|
||||
return aspect.texels > 0 && aspect.renderPassMatches == aspect.texels;
|
||||
}
|
||||
|
||||
// An aspect only counts where its control holds; a format is conclusive when any of its
|
||||
// aspects is. The render pass resolves every aspect of the format in one pass, so one aspect
|
||||
// it fails is the defect for the whole arm.
|
||||
inline WireDepthResolveProbeVerdict EvaluateWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) {
|
||||
if (!measurement.ran || measurement.fenceWaitTimedOut) return WireDepthResolveProbeVerdict::NotRun;
|
||||
Bool conclusive = false, broken = false;
|
||||
for (const WireDepthResolveFormatReading& reading : measurement.formats) {
|
||||
if (!reading.ran) continue;
|
||||
for (const WireDepthResolveAspectReading* aspect : {&reading.depth, &reading.stencil}) {
|
||||
if (!WireDepthResolveControlHolds(*aspect)) continue;
|
||||
conclusive = true;
|
||||
if (!WireDepthResolveSubjectHolds(*aspect)) broken = true;
|
||||
}
|
||||
}
|
||||
if (broken) return WireDepthResolveProbeVerdict::RenderPassResolveBroken;
|
||||
return conclusive ? WireDepthResolveProbeVerdict::Clean : WireDepthResolveProbeVerdict::Inconclusive;
|
||||
}
|
||||
|
||||
// THE TEST KNOB, read by the SERVER (split and spawn arms only; the tcp server is a lane fixture
|
||||
// whose environment no entry reaches - spawn_lane_parity.py's MAGMA_SERVER_ENV_KNOB_NO_TCP).
|
||||
// Lavapipe resolves correctly through both arms, so no host lane can reach the verdict that
|
||||
// matters on the Redmi. MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean replaces the MEASUREMENT
|
||||
// with a canned one that says exactly that, and nothing else: the canned measurement still goes
|
||||
// through EvaluateWireDepthResolveProbe and the same member, so an entry that forces `bug` is red
|
||||
// when the evaluation stops detecting the defect.
|
||||
//
|
||||
// A canned measurement cannot see the REAL probe - its recording, readback and tally - stop
|
||||
// detecting the defect, and on lavapipe the real subject is clean, so nothing would. The third
|
||||
// value, MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject, is that negative control: the REAL
|
||||
// probe runs (ChooseWireDepthResolveArm treats it as Measure) and the caller passes it on as
|
||||
// WireDepthResolveProbeContext::elideSubject, so the probe records no render-pass resolve and
|
||||
// the real readback keeps the sentinel while the shader control still runs. The
|
||||
// DirectVulkan.{Split,Spawn}.MsResolveElide. entries then require the defect verdict from the
|
||||
// measurement itself.
|
||||
enum class WireDepthResolveProbeKnob : Uint8 { Measure, ForceBug, ForceClean, ElideSubject, Unrecognised };
|
||||
|
||||
inline WireDepthResolveProbeKnob ParseWireDepthResolveProbeKnob(const char* value) {
|
||||
if (value == nullptr || *value == '\0') return WireDepthResolveProbeKnob::Measure;
|
||||
if (std::strcmp(value, "bug") == 0) return WireDepthResolveProbeKnob::ForceBug;
|
||||
if (std::strcmp(value, "clean") == 0) return WireDepthResolveProbeKnob::ForceClean;
|
||||
if (std::strcmp(value, "elide-subject") == 0) return WireDepthResolveProbeKnob::ElideSubject;
|
||||
return WireDepthResolveProbeKnob::Unrecognised;
|
||||
}
|
||||
|
||||
// One packed format's worth of readings: the control resolved both aspects; the render pass
|
||||
// either did too (`renderPassWrites`) or left the sentinel everywhere.
|
||||
inline WireDepthResolveProbeMeasurement CannedWireDepthResolveProbeMeasurement(Bool renderPassWrites) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
measurement.ran = true;
|
||||
measurement.fromKnob = true;
|
||||
WireDepthResolveFormatReading reading;
|
||||
reading.name = "MGITEST_MAGMA_DEPTH_RESOLVE_PROBE";
|
||||
reading.samples = 4;
|
||||
reading.ran = true;
|
||||
const auto fill = [&](WireDepthResolveAspectReading& aspect, Uint32 expected, Uint32 sentinel) {
|
||||
aspect.measured = true;
|
||||
aspect.texels = 16;
|
||||
aspect.expected = expected;
|
||||
aspect.sentinel = sentinel;
|
||||
aspect.renderPassMatches = renderPassWrites ? 16 : 0;
|
||||
aspect.renderPassSentinels = renderPassWrites ? 0 : 16;
|
||||
aspect.renderPassFirst = renderPassWrites ? expected : sentinel;
|
||||
aspect.shaderRan = true;
|
||||
aspect.shaderMatches = 16;
|
||||
aspect.shaderFirst = expected;
|
||||
};
|
||||
fill(reading.depth, 0x400000u, 0xBFFFFFu);
|
||||
fill(reading.stencil, 0x5Au, 0xA5u);
|
||||
measurement.formats.push_back(Move(reading));
|
||||
return measurement;
|
||||
}
|
||||
|
||||
struct WireDepthResolveArmChoice {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
WireDepthResolveProbeVerdict verdict = WireDepthResolveProbeVerdict::NotRun;
|
||||
// m_wirePreferShaderDepthResolve: the shader pass goes first, the render pass is the
|
||||
// fallback for what the shader cannot write.
|
||||
Bool preferShader = false;
|
||||
};
|
||||
|
||||
// The whole decision, with the Vulkan half behind `measure` (called at most once, and only when
|
||||
// the knob does not decide and the render-pass arm exists at all: without it the shader pass
|
||||
// is the only arm and there is no order to choose). ElideSubject does not decide: it is
|
||||
// measured like Measure, and the elision is the measuring callable's to apply.
|
||||
template <typename MeasureFn>
|
||||
WireDepthResolveArmChoice ChooseWireDepthResolveArm(WireDepthResolveProbeKnob knob, Bool renderPassArmAvailable,
|
||||
MeasureFn&& measure) {
|
||||
WireDepthResolveArmChoice choice;
|
||||
if (knob == WireDepthResolveProbeKnob::ForceBug || knob == WireDepthResolveProbeKnob::ForceClean) {
|
||||
choice.measurement = CannedWireDepthResolveProbeMeasurement(knob == WireDepthResolveProbeKnob::ForceClean);
|
||||
} else if (renderPassArmAvailable) {
|
||||
choice.measurement = measure();
|
||||
}
|
||||
choice.verdict = EvaluateWireDepthResolveProbe(choice.measurement);
|
||||
choice.preferShader = choice.verdict == WireDepthResolveProbeVerdict::RenderPassResolveBroken;
|
||||
return choice;
|
||||
}
|
||||
|
||||
// WHOSE VERDICT IT IS (codex closeout finding 7). The verdict is a property of ONE device and
|
||||
// driver, and a process can hold more than one: inproc tears a renderer down and initializes
|
||||
// another, possibly on a different physical device or under a different driver. The choice was
|
||||
// a function-static - the first renderer's answer for every renderer after it - so a second
|
||||
// device skipped its own probe and took an order measured elsewhere. It is now memoized per
|
||||
// IDENTITY: the device (vendor, device, driver version, pipeline-cache UUID - the last changes
|
||||
// with the driver build even where the version number does not) plus the two inputs the choice
|
||||
// itself depends on (whether a render-pass arm exists to judge, and the test knob). The same
|
||||
// identity is still probed once per process; a different one is probed for itself.
|
||||
struct WireDepthResolveDeviceIdentity {
|
||||
Uint32 vendorID = 0;
|
||||
Uint32 deviceID = 0;
|
||||
Uint32 driverVersion = 0;
|
||||
Uint8 pipelineCacheUUID[16] = {};
|
||||
Bool renderPassArmAvailable = false;
|
||||
WireDepthResolveProbeKnob knob = WireDepthResolveProbeKnob::Measure;
|
||||
|
||||
Bool operator==(const WireDepthResolveDeviceIdentity& other) const {
|
||||
return vendorID == other.vendorID && deviceID == other.deviceID && driverVersion == other.driverVersion &&
|
||||
std::memcmp(pipelineCacheUUID, other.pipelineCacheUUID, sizeof(pipelineCacheUUID)) == 0 &&
|
||||
renderPassArmAvailable == other.renderPassArmAvailable && knob == other.knob;
|
||||
}
|
||||
};
|
||||
|
||||
// The renderer's identity from its device's VkPhysicalDeviceProperties - the ONE place the fields
|
||||
// are filled (VulkanRenderer::ArmWireDepthResolveOrder calls it), so the unit test proves that each
|
||||
// property reaches the key: with only the cache tested, a field left out here stayed green.
|
||||
inline WireDepthResolveDeviceIdentity MakeWireDepthResolveDeviceIdentity(const VkPhysicalDeviceProperties& properties,
|
||||
Bool renderPassArmAvailable,
|
||||
WireDepthResolveProbeKnob knob) {
|
||||
WireDepthResolveDeviceIdentity identity;
|
||||
identity.vendorID = properties.vendorID;
|
||||
identity.deviceID = properties.deviceID;
|
||||
identity.driverVersion = properties.driverVersion;
|
||||
static_assert(sizeof(identity.pipelineCacheUUID) == sizeof(properties.pipelineCacheUUID));
|
||||
std::memcpy(identity.pipelineCacheUUID, properties.pipelineCacheUUID, sizeof(identity.pipelineCacheUUID));
|
||||
identity.renderPassArmAvailable = renderPassArmAvailable;
|
||||
identity.knob = knob;
|
||||
return identity;
|
||||
}
|
||||
|
||||
class WireDepthResolveArmCache {
|
||||
public:
|
||||
// The identity's memoized choice, or `decide()`'s - called at most once per identity, under
|
||||
// the cache's lock (a probe records on the device's queue; two renderers of one identity
|
||||
// must not both run it). `decided`, when given, says whether this call ran `decide`.
|
||||
template <typename DecideFn>
|
||||
WireDepthResolveArmChoice Resolve(const WireDepthResolveDeviceIdentity& identity, DecideFn&& decide,
|
||||
Bool* decided = nullptr) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& entry : m_entries) {
|
||||
if (entry.first == identity) {
|
||||
if (decided) *decided = false;
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
WireDepthResolveArmChoice choice = decide();
|
||||
m_entries.emplace_back(identity, choice);
|
||||
if (decided) *decided = true;
|
||||
return choice;
|
||||
}
|
||||
|
||||
SizeT Size() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_entries.size();
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
Vector<std::pair<WireDepthResolveDeviceIdentity, WireDepthResolveArmChoice>> m_entries;
|
||||
};
|
||||
|
||||
// One format's readings, both arms: "D24_UNORM_S8_UINT x4: depth expected 0x400000 sentinel
|
||||
// 0xbfffff: render pass 0/16 (first 0xbfffff, sentinel 16), shader control 16/16 (first
|
||||
// 0x400000) / stencil ...".
|
||||
inline String DescribeWireDepthResolveFormat(const WireDepthResolveFormatReading& reading) {
|
||||
const auto aspectText = [](const char* name, const WireDepthResolveAspectReading& aspect) {
|
||||
String text = format("{} expected 0x{:x} sentinel 0x{:x}: render pass {}/{} (first 0x{:x}, sentinel {}), ",
|
||||
name, aspect.expected, aspect.sentinel, aspect.renderPassMatches, aspect.texels,
|
||||
aspect.renderPassFirst, aspect.renderPassSentinels);
|
||||
if (aspect.shaderRan) {
|
||||
text += format("shader control {}/{} (first 0x{:x})", aspect.shaderMatches, aspect.texels,
|
||||
aspect.shaderFirst);
|
||||
} else {
|
||||
text += "no shader control";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
String out = reading.name;
|
||||
if (!reading.ran) return out + " skipped (" + reading.skipReason + ")";
|
||||
out += format(" x{}: ", reading.samples);
|
||||
if (reading.depth.measured) out += aspectText("depth", reading.depth);
|
||||
if (reading.depth.measured && reading.stencil.measured) out += " / ";
|
||||
if (reading.stencil.measured) out += aspectText("stencil", reading.stencil);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every format, "; "-separated, for the POST row.
|
||||
inline String DescribeWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) {
|
||||
if (!measurement.ran) {
|
||||
return measurement.failureReason.empty() ? String("not run") : "not run (" + measurement.failureReason + ")";
|
||||
}
|
||||
String out;
|
||||
for (const WireDepthResolveFormatReading& reading : measurement.formats) {
|
||||
if (!out.empty()) out += "; ";
|
||||
out += DescribeWireDepthResolveFormat(reading);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,992 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// See WireDepthResolveProbe.h for the arms and the probe's shape, WireDepthResolveArm.h for the
|
||||
// verdict. The plumbing is the PRIMITIVES_GENERATED probe's (MG_Util/SelfTest/
|
||||
// PrimitivesGeneratedNoXfbProbe.cpp): one throwaway command buffer, a bounded fence wait that
|
||||
// leaks rather than idle-waits a hung GPU, teardown on every other path.
|
||||
|
||||
#include "WireDepthResolveProbe.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#include "WireMultisampleResolveSpirv.h"
|
||||
|
||||
VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format) {
|
||||
if (format == VK_FORMAT_S8_UINT) return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
if (format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT ||
|
||||
format == VK_FORMAT_D32_SFLOAT_S8_UINT)
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects,
|
||||
Uint32 level, Uint32 layer) {
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = image;
|
||||
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
info.format = format;
|
||||
info.subresourceRange = {aspects, level, 1, layer, 1};
|
||||
return info;
|
||||
}
|
||||
|
||||
VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device,
|
||||
VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass) {
|
||||
const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(format);
|
||||
VkAttachmentDescription2 attachments[2]{};
|
||||
for (Uint32 i = 0; i < 2; ++i) {
|
||||
auto& attachment = attachments[i];
|
||||
attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2;
|
||||
attachment.format = format;
|
||||
attachment.samples = i == 0 ? samples : VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = attachment.stencilLoadOp =
|
||||
i == 0 ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
}
|
||||
VkAttachmentReference2 sourceReference{VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2};
|
||||
sourceReference.attachment = 0;
|
||||
sourceReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
sourceReference.aspectMask = formatAspects;
|
||||
VkAttachmentReference2 resolveReference = sourceReference;
|
||||
resolveReference.attachment = 1;
|
||||
VkSubpassDescriptionDepthStencilResolve resolve{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE};
|
||||
resolve.depthResolveMode =
|
||||
(formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE;
|
||||
resolve.stencilResolveMode =
|
||||
(formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE;
|
||||
resolve.pDepthStencilResolveAttachment = &resolveReference;
|
||||
VkSubpassDescription2 subpass{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2};
|
||||
subpass.pNext = &resolve;
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &sourceReference;
|
||||
VkRenderPassCreateInfo2 passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2};
|
||||
passInfo.attachmentCount = 2;
|
||||
passInfo.pAttachments = attachments;
|
||||
passInfo.subpassCount = 1;
|
||||
passInfo.pSubpasses = &subpass;
|
||||
return createRenderPass2(device, &passInfo, nullptr, pass);
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit) {
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
return vkCreateShaderModule(device, &info, nullptr, &module);
|
||||
};
|
||||
VkResult result =
|
||||
shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), kit.vertex);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
result = shader(kWireMultisampleDepthResolveFragmentSpirv, sizeof(kWireMultisampleDepthResolveFragmentSpirv),
|
||||
kit.depthFragment);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the
|
||||
// StencilExportEXT capability, and handing a module with an unsupported capability to
|
||||
// vkCreateShaderModule is undefined behaviour - not an error this could catch.
|
||||
if (shaderStencilExport) {
|
||||
result = shader(kWireMultisampleStencilResolveFragmentSpirv,
|
||||
sizeof(kWireMultisampleStencilResolveFragmentSpirv), kit.stencilFragment);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
}
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
result = vkCreateDescriptorSetLayout(device, &layout, nullptr, &kit.descriptorLayout);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and its
|
||||
// rectangle from the scissor, so neither stage takes an input.
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &kit.descriptorLayout;
|
||||
result = vkCreatePipelineLayout(device, &pipelineLayout, nullptr, &kit.pipelineLayout);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// NEAREST: texelFetch ignores the sampler's filter, and most devices do not advertise
|
||||
// linear filtering for a depth format anyway. The sampler exists only because GLSL's
|
||||
// sampler2DMS is a COMBINED image sampler.
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
return vkCreateSampler(device, &sampler, nullptr, &kit.nearest);
|
||||
}
|
||||
|
||||
void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit) {
|
||||
vkDestroySampler(device, kit.nearest, nullptr);
|
||||
vkDestroyPipelineLayout(device, kit.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(device, kit.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(device, kit.vertex, nullptr);
|
||||
vkDestroyShaderModule(device, kit.depthFragment, nullptr);
|
||||
vkDestroyShaderModule(device, kit.stencilFragment, nullptr);
|
||||
kit = {};
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass) {
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for the
|
||||
// aspect it owns, and the caller copies only that aspect out. The other aspect of a packed
|
||||
// format is left undefined on purpose - it is never read, and LOADing it would order this
|
||||
// pass behind a write nobody is waiting for.
|
||||
attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &depthStencil;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = 1;
|
||||
info.pAttachments = &attachment;
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
return vkCreateRenderPass(device, &info, nullptr, pass);
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass,
|
||||
VkImageAspectFlags aspect, VkPipeline* pipeline) {
|
||||
const VkShaderModule fragment =
|
||||
aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? kit.stencilFragment : kit.depthFragment;
|
||||
if (fragment == VK_NULL_HANDLE || kit.vertex == VK_NULL_HANDLE) return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = kit.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencilState{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencilState.minDepthBounds = 0.0f;
|
||||
depthStencilState.maxDepthBounds = 1.0f;
|
||||
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
// ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever
|
||||
// DONT_CARE left behind would discard some of it.
|
||||
depthStencilState.depthTestEnable = VK_TRUE;
|
||||
depthStencilState.depthWriteEnable = VK_TRUE;
|
||||
depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||
} else {
|
||||
// REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes the
|
||||
// shader-exported reference the value that lands. The static `reference` is overridden
|
||||
// by gl_FragStencilRefARB, which is the whole point of the extension.
|
||||
depthStencilState.stencilTestEnable = VK_TRUE;
|
||||
VkStencilOpState op{};
|
||||
op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE;
|
||||
op.compareOp = VK_COMPARE_OP_ALWAYS;
|
||||
op.compareMask = op.writeMask = 0xFFu;
|
||||
op.reference = 0;
|
||||
depthStencilState.front = depthStencilState.back = op;
|
||||
}
|
||||
// No colour attachment in this render pass, so no attachment states either.
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
info.stageCount = 2;
|
||||
info.pStages = stages;
|
||||
info.pVertexInputState = &vertex;
|
||||
info.pInputAssemblyState = &assembly;
|
||||
info.pViewportState = &viewport;
|
||||
info.pRasterizationState = &raster;
|
||||
info.pMultisampleState = &samples;
|
||||
info.pDepthStencilState = &depthStencilState;
|
||||
info.pColorBlendState = &blend;
|
||||
info.pDynamicState = &dynamic;
|
||||
info.layout = kit.pipelineLayout;
|
||||
info.renderPass = pass;
|
||||
return vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, pipeline);
|
||||
}
|
||||
|
||||
void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit,
|
||||
VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer,
|
||||
VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = pass;
|
||||
begin.framebuffer = framebuffer;
|
||||
// The RECTANGLE is the render area, exactly as the render-pass arm's renderArea is - the
|
||||
// scratch outside it is never read.
|
||||
begin.renderArea = rect;
|
||||
vkCmdBeginRenderPass(commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
// THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not
|
||||
// interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the
|
||||
// clip-space-to-attachment mapping has to be the identity for every rectangle. A viewport
|
||||
// shrunk onto the rect would slide the whole image instead of moving a window over it.
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(attachmentExtent.width),
|
||||
static_cast<float>(attachmentExtent.height), 0.0f, 1.0f};
|
||||
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(commandBuffer, 0, 1, &rect);
|
||||
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, kit.pipelineLayout, 0, 1,
|
||||
&descriptorSet, 0, nullptr);
|
||||
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr Uint32 kProbeExtent = 4;
|
||||
constexpr Uint32 kProbeTexels = kProbeExtent * kProbeExtent;
|
||||
// Every Vulkan device must support 4 samples on a depth/stencil attachment
|
||||
// (framebufferDepthSampleCounts), and the CTS case the probe stands for fails at 2 and 4.
|
||||
constexpr VkSampleCountFlagBits kProbeSamples = VK_SAMPLE_COUNT_4_BIT;
|
||||
constexpr Float kClearDepth = 0.25f;
|
||||
constexpr Float kSentinelDepth = 0.75f;
|
||||
constexpr Uint32 kClearStencil = 0x5Au;
|
||||
constexpr Uint32 kSentinelStencil = 0xA5u;
|
||||
constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull;
|
||||
|
||||
// The formats the wire maps GL's depth/stencil internal formats onto (VkTextureManager),
|
||||
// packed first: the case that set all this off failed on every one of them.
|
||||
struct ProbeFormat {
|
||||
VkFormat format;
|
||||
const char* name;
|
||||
};
|
||||
constexpr ProbeFormat kProbeFormats[] = {
|
||||
{VK_FORMAT_D24_UNORM_S8_UINT, "D24_UNORM_S8_UINT"},
|
||||
{VK_FORMAT_D32_SFLOAT_S8_UINT, "D32_SFLOAT_S8_UINT"},
|
||||
{VK_FORMAT_D16_UNORM, "D16_UNORM"},
|
||||
{VK_FORMAT_X8_D24_UNORM_PACK32, "X8_D24_UNORM_PACK32"},
|
||||
{VK_FORMAT_D32_SFLOAT, "D32_SFLOAT"},
|
||||
{VK_FORMAT_S8_UINT, "S8_UINT"},
|
||||
};
|
||||
|
||||
// The usages production gives the two images: a wire multisample depth/stencil attachment
|
||||
// (VkTextureManager: SAMPLED | DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC | TRANSFER_DST) and
|
||||
// ResolveWireDepthStencil's resolve scratch (DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC).
|
||||
constexpr VkImageUsageFlags kSourceUsage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
constexpr VkImageUsageFlags kScratchUsage =
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
Bool IsFloatDepth(VkFormat format) {
|
||||
return format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
}
|
||||
|
||||
Uint32 DepthCopyBytes(VkFormat format) { return format == VK_FORMAT_D16_UNORM ? 2u : 4u; }
|
||||
|
||||
Uint32 EncodeDepth(VkFormat format, Float depth) {
|
||||
if (IsFloatDepth(format)) return std::bit_cast<Uint32>(depth);
|
||||
const double scale = format == VK_FORMAT_D16_UNORM ? 65535.0 : 16777215.0;
|
||||
return static_cast<Uint32>(std::lround(static_cast<double>(depth) * scale));
|
||||
}
|
||||
|
||||
Uint32 ReadDepthWord(VkFormat format, const Uint8* texel) {
|
||||
if (format == VK_FORMAT_D16_UNORM) {
|
||||
Uint16 word = 0;
|
||||
std::memcpy(&word, texel, sizeof(word));
|
||||
return word;
|
||||
}
|
||||
Uint32 word = 0;
|
||||
std::memcpy(&word, texel, sizeof(word));
|
||||
// A D24 aspect copies out as a 32-bit word whose top byte is undefined.
|
||||
return IsFloatDepth(format) ? word : (word & 0x00FFFFFFu);
|
||||
}
|
||||
|
||||
Uint32 WordDistance(Uint32 a, Uint32 b) { return a > b ? a - b : b - a; }
|
||||
|
||||
struct ProbeImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct ControlPass {
|
||||
Bool ready = false;
|
||||
ProbeImage target;
|
||||
VkRenderPass pass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkImageView sampledView = VK_NULL_HANDLE;
|
||||
VkImageView targetView = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
};
|
||||
|
||||
struct FormatWork {
|
||||
SizeT reading = 0;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
ProbeImage subjectSource, controlSource, subjectTarget;
|
||||
VkRenderPass resolvePass = VK_NULL_HANDLE;
|
||||
VkImageView resolveViews[2]{};
|
||||
VkFramebuffer resolveFramebuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize subjectDepthOffset = 0, subjectStencilOffset = 0;
|
||||
ControlPass depthControl, stencilControl;
|
||||
};
|
||||
|
||||
// Everything the probe creates, destroyed together (or leaked together on a timeout).
|
||||
struct ProbeObjects {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
Vector<VkImage> images;
|
||||
Vector<VkDeviceMemory> memories;
|
||||
Vector<VkImageView> views;
|
||||
Vector<VkRenderPass> passes;
|
||||
Vector<VkFramebuffer> framebuffers;
|
||||
Vector<VkPipeline> pipelines;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
WireShaderResolveKit kit;
|
||||
|
||||
void Destroy() {
|
||||
for (VkPipeline pipeline : pipelines) vkDestroyPipeline(device, pipeline, nullptr);
|
||||
for (VkFramebuffer framebuffer : framebuffers) vkDestroyFramebuffer(device, framebuffer, nullptr);
|
||||
for (VkRenderPass pass : passes) vkDestroyRenderPass(device, pass, nullptr);
|
||||
for (VkImageView view : views) vkDestroyImageView(device, view, nullptr);
|
||||
for (VkImage image : images) vkDestroyImage(device, image, nullptr);
|
||||
vkDestroyBuffer(device, buffer, nullptr);
|
||||
for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr);
|
||||
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
|
||||
vkDestroyCommandPool(device, commandPool, nullptr);
|
||||
vkDestroyFence(device, fence, nullptr);
|
||||
DestroyWireShaderResolveKit(device, kit);
|
||||
}
|
||||
};
|
||||
|
||||
Optional<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
|
||||
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
|
||||
Optional<Uint32> fallback;
|
||||
for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) {
|
||||
if (!(allowed & (1u << i))) continue;
|
||||
const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags;
|
||||
if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue;
|
||||
if ((flags & required) != required) continue;
|
||||
if ((flags & wanted) == wanted) return i;
|
||||
if (!fallback) fallback = i;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void ImageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageAspectFlags aspects,
|
||||
VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStage,
|
||||
VkAccessFlags srcAccess, VkPipelineStageFlags dstStage, VkAccessFlags dstAccess) {
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = srcAccess;
|
||||
barrier.dstAccessMask = dstAccess;
|
||||
barrier.oldLayout = oldLayout;
|
||||
barrier.newLayout = newLayout;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = image;
|
||||
barrier.subresourceRange = {aspects, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
measurement.subjectElided = context.elideSubject;
|
||||
const VkDevice device = context.device;
|
||||
if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE || context.createRenderPass2 == nullptr) {
|
||||
measurement.failureReason = "no device, queue or vkCreateRenderPass2";
|
||||
return measurement;
|
||||
}
|
||||
ProbeObjects objects;
|
||||
objects.device = device;
|
||||
Bool leak = false;
|
||||
struct Teardown {
|
||||
ProbeObjects& objects;
|
||||
Bool& leak;
|
||||
~Teardown() {
|
||||
if (!leak) objects.Destroy();
|
||||
}
|
||||
} teardown{objects, leak};
|
||||
const auto fail = [&](String reason) {
|
||||
measurement.ran = false;
|
||||
measurement.failureReason = Move(reason);
|
||||
return measurement;
|
||||
};
|
||||
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties);
|
||||
const auto makeImage = [&](VkFormat format, VkSampleCountFlagBits samples, VkImageUsageFlags usage,
|
||||
ProbeImage& out) -> Bool {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format;
|
||||
info.extent = {kProbeExtent, kProbeExtent, 1};
|
||||
info.mipLevels = info.arrayLayers = 1;
|
||||
info.samples = samples;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = usage;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
if (vkCreateImage(device, &info, nullptr, &out.image) != VK_SUCCESS) return false;
|
||||
objects.images.push_back(out.image);
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(device, out.image, &requirements);
|
||||
const Optional<Uint32> type = PickMemoryType(memoryProperties, requirements.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0);
|
||||
if (!type) return false;
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size;
|
||||
allocate.memoryTypeIndex = *type;
|
||||
if (vkAllocateMemory(device, &allocate, nullptr, &out.memory) != VK_SUCCESS) return false;
|
||||
objects.memories.push_back(out.memory);
|
||||
return vkBindImageMemory(device, out.image, out.memory, 0) == VK_SUCCESS;
|
||||
};
|
||||
const auto makeView = [&](const VkImageViewCreateInfo& info, VkImageView& out) -> Bool {
|
||||
if (vkCreateImageView(device, &info, nullptr, &out) != VK_SUCCESS) return false;
|
||||
objects.views.push_back(out);
|
||||
return true;
|
||||
};
|
||||
const auto makeFramebuffer = [&](VkRenderPass pass, const VkImageView* views, Uint32 count,
|
||||
VkFramebuffer& out) -> Bool {
|
||||
VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
info.renderPass = pass;
|
||||
info.attachmentCount = count;
|
||||
info.pAttachments = views;
|
||||
info.width = info.height = kProbeExtent;
|
||||
info.layers = 1;
|
||||
if (vkCreateFramebuffer(device, &info, nullptr, &out) != VK_SUCCESS) return false;
|
||||
objects.framebuffers.push_back(out);
|
||||
return true;
|
||||
};
|
||||
// The wire's clear (WireFramebuffer.inc, ClearWireFramebuffer): a LOAD/STORE pass over every
|
||||
// aspect of the format with vkCmdClearAttachments inside it. Recorded later; built now.
|
||||
struct ClearPass {
|
||||
VkRenderPass pass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
Float depth = 0.0f;
|
||||
Uint32 stencil = 0;
|
||||
};
|
||||
Vector<ClearPass> clears;
|
||||
const auto makeClear = [&](const ProbeImage& image, VkFormat format, VkSampleCountFlagBits samples,
|
||||
Float depth, Uint32 stencil) -> Bool {
|
||||
const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format);
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
if (!makeView(WireDepthStencilViewInfo(image.image, format, aspects, 0, 0), view)) return false;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = samples;
|
||||
attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &reference;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = 1;
|
||||
info.pAttachments = &attachment;
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
ClearPass clear;
|
||||
if (vkCreateRenderPass(device, &info, nullptr, &clear.pass) != VK_SUCCESS) return false;
|
||||
objects.passes.push_back(clear.pass);
|
||||
if (!makeFramebuffer(clear.pass, &view, 1, clear.framebuffer)) return false;
|
||||
clear.aspects = aspects;
|
||||
clear.depth = depth;
|
||||
clear.stencil = stencil;
|
||||
clears.push_back(clear);
|
||||
return true;
|
||||
};
|
||||
|
||||
// THE CONTROL'S FORMAT-INDEPENDENT HALF. A kit that cannot be made leaves every control
|
||||
// un-run, which the verdict reads as INCONCLUSIVE - never as the bug.
|
||||
String kitFailure;
|
||||
if (const VkResult result = CreateWireShaderResolveKit(device, context.shaderStencilExport, objects.kit);
|
||||
result != VK_SUCCESS)
|
||||
kitFailure = format("shader resolve kit creation failed (VkResult {})", static_cast<Int>(result));
|
||||
|
||||
// Readback layout: every region on a 16-byte boundary (a depth/stencil buffer copy wants a
|
||||
// multiple of 4).
|
||||
VkDeviceSize bufferBytes = 0;
|
||||
const auto reserve = [&](VkDeviceSize bytes) {
|
||||
const VkDeviceSize offset = bufferBytes;
|
||||
bufferBytes += (bytes + 15u) & ~VkDeviceSize{15u};
|
||||
return offset;
|
||||
};
|
||||
|
||||
Vector<FormatWork> work;
|
||||
for (const ProbeFormat& probeFormat : kProbeFormats) {
|
||||
WireDepthResolveFormatReading reading;
|
||||
reading.name = probeFormat.name;
|
||||
reading.format = static_cast<Int32>(probeFormat.format);
|
||||
reading.samples = static_cast<Uint32>(kProbeSamples);
|
||||
const SizeT readingIndex = measurement.formats.size();
|
||||
measurement.formats.push_back(reading);
|
||||
auto& slot = measurement.formats.back();
|
||||
// A skipped format records nothing: the clears it had already built are dropped with it
|
||||
// (their images stay in `objects` and are only destroyed).
|
||||
const SizeT clearsBefore = clears.size();
|
||||
const auto skip = [&](String reason) {
|
||||
slot.skipReason = Move(reason);
|
||||
clears.resize(clearsBefore);
|
||||
};
|
||||
|
||||
const VkFormat format = probeFormat.format;
|
||||
const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format);
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(context.physicalDevice, format, &formatProperties);
|
||||
constexpr VkFormatFeatureFlags kRequired =
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
if ((formatProperties.optimalTilingFeatures & kRequired) != kRequired) {
|
||||
skip("not a sampled depth/stencil attachment format here");
|
||||
continue;
|
||||
}
|
||||
VkImageFormatProperties imageProperties{};
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, kSourceUsage, 0,
|
||||
&imageProperties) != VK_SUCCESS ||
|
||||
!(imageProperties.sampleCounts & kProbeSamples)) {
|
||||
skip("no 4x multisample image with the wire's attachment usage");
|
||||
continue;
|
||||
}
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, kScratchUsage, 0,
|
||||
&imageProperties) != VK_SUCCESS) {
|
||||
skip("no single-sample image with the resolve scratch's usage");
|
||||
continue;
|
||||
}
|
||||
// ResolveWireDepthStencil's own test for the render-pass arm, per format.
|
||||
if (((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) &&
|
||||
!(context.depthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) ||
|
||||
((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) &&
|
||||
!(context.stencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT))) {
|
||||
skip("the render-pass arm is not taken for this format (no SAMPLE_ZERO resolve mode)");
|
||||
continue;
|
||||
}
|
||||
|
||||
FormatWork item;
|
||||
item.reading = readingIndex;
|
||||
item.format = format;
|
||||
item.aspects = aspects;
|
||||
if (!makeImage(format, kProbeSamples, kSourceUsage, item.subjectSource) ||
|
||||
!makeImage(format, kProbeSamples, kSourceUsage, item.controlSource) ||
|
||||
!makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, item.subjectTarget)) {
|
||||
skip("image creation failed");
|
||||
continue;
|
||||
}
|
||||
if (CreateWireDepthStencilResolvePass(context.createRenderPass2, device, format, kProbeSamples,
|
||||
&item.resolvePass) != VK_SUCCESS) {
|
||||
skip("the render-pass arm's vkCreateRenderPass2 failed");
|
||||
continue;
|
||||
}
|
||||
objects.passes.push_back(item.resolvePass);
|
||||
if (!makeView(WireDepthStencilViewInfo(item.subjectSource.image, format, aspects, 0, 0),
|
||||
item.resolveViews[0]) ||
|
||||
!makeView(WireDepthStencilViewInfo(item.subjectTarget.image, format, aspects, 0, 0),
|
||||
item.resolveViews[1]) ||
|
||||
!makeFramebuffer(item.resolvePass, item.resolveViews, 2, item.resolveFramebuffer)) {
|
||||
skip("the render-pass arm's views or framebuffer could not be created");
|
||||
continue;
|
||||
}
|
||||
if (!makeClear(item.subjectSource, format, kProbeSamples, kClearDepth, kClearStencil) ||
|
||||
!makeClear(item.controlSource, format, kProbeSamples, kClearDepth, kClearStencil) ||
|
||||
!makeClear(item.subjectTarget, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil)) {
|
||||
skip("the clear passes could not be created");
|
||||
continue;
|
||||
}
|
||||
// The control, one target per aspect: the shader pass's attachment is DONT_CARE on both
|
||||
// aspects, so a second aspect's pass into the same target would discard the first.
|
||||
const auto makeControl = [&](VkImageAspectFlags aspect, ControlPass& control) {
|
||||
if (!kitFailure.empty()) return;
|
||||
if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && objects.kit.stencilFragment == VK_NULL_HANDLE) return;
|
||||
if (!makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, control.target)) return;
|
||||
if (CreateWireShaderResolveRenderPass(device, format, &control.pass) != VK_SUCCESS) return;
|
||||
objects.passes.push_back(control.pass);
|
||||
if (CreateWireShaderResolvePipeline(device, objects.kit, control.pass, aspect, &control.pipeline) !=
|
||||
VK_SUCCESS)
|
||||
return;
|
||||
objects.pipelines.push_back(control.pipeline);
|
||||
if (!makeView(WireDepthStencilViewInfo(item.controlSource.image, format, aspect, 0, 0),
|
||||
control.sampledView) ||
|
||||
!makeView(WireDepthStencilViewInfo(control.target.image, format, aspects, 0, 0),
|
||||
control.targetView) ||
|
||||
!makeFramebuffer(control.pass, &control.targetView, 1, control.framebuffer))
|
||||
return;
|
||||
if (!makeClear(control.target, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil))
|
||||
return;
|
||||
control.ready = true;
|
||||
};
|
||||
if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) makeControl(VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl);
|
||||
if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) makeControl(VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl);
|
||||
|
||||
if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
item.subjectDepthOffset = reserve(kProbeTexels * DepthCopyBytes(format));
|
||||
if (item.depthControl.ready) item.depthControl.offset = reserve(kProbeTexels * DepthCopyBytes(format));
|
||||
}
|
||||
if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
||||
item.subjectStencilOffset = reserve(kProbeTexels);
|
||||
if (item.stencilControl.ready) item.stencilControl.offset = reserve(kProbeTexels);
|
||||
}
|
||||
slot.ran = true;
|
||||
work.push_back(item);
|
||||
}
|
||||
if (work.empty()) return fail("no format could be probed");
|
||||
|
||||
// Descriptor sets for the controls, one per draw (the production shader arm's own rule).
|
||||
Uint32 controlDraws = 0;
|
||||
for (const FormatWork& item : work)
|
||||
controlDraws += (item.depthControl.ready ? 1u : 0u) + (item.stencilControl.ready ? 1u : 0u);
|
||||
if (controlDraws > 0) {
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, controlDraws};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = controlDraws;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
if (vkCreateDescriptorPool(device, &pool, nullptr, &objects.descriptorPool) != VK_SUCCESS)
|
||||
return fail("vkCreateDescriptorPool failed");
|
||||
for (FormatWork& item : work) {
|
||||
for (ControlPass* control : {&item.depthControl, &item.stencilControl}) {
|
||||
if (!control->ready) continue;
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = objects.descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &objects.kit.descriptorLayout;
|
||||
if (vkAllocateDescriptorSets(device, &alloc, &control->set) != VK_SUCCESS)
|
||||
return fail("vkAllocateDescriptorSets failed");
|
||||
const VkDescriptorImageInfo sampled{objects.kit.nearest, control->sampledView,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = control->set;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The host-visible readback buffer.
|
||||
VkDeviceMemory bufferMemory = VK_NULL_HANDLE;
|
||||
Bool bufferCoherent = false;
|
||||
{
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = bufferBytes;
|
||||
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateBuffer(device, &info, nullptr, &objects.buffer) != VK_SUCCESS)
|
||||
return fail("vkCreateBuffer failed");
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetBufferMemoryRequirements(device, objects.buffer, &requirements);
|
||||
const Optional<Uint32> type =
|
||||
PickMemoryType(memoryProperties, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
|
||||
if (!type) return fail("no host-visible memory type for the readback buffer");
|
||||
bufferCoherent = (memoryProperties.memoryTypes[*type].propertyFlags &
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size;
|
||||
allocate.memoryTypeIndex = *type;
|
||||
if (vkAllocateMemory(device, &allocate, nullptr, &bufferMemory) != VK_SUCCESS)
|
||||
return fail("vkAllocateMemory (readback) failed");
|
||||
objects.memories.push_back(bufferMemory);
|
||||
if (vkBindBufferMemory(device, objects.buffer, bufferMemory, 0) != VK_SUCCESS)
|
||||
return fail("vkBindBufferMemory failed");
|
||||
}
|
||||
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
|
||||
if (vkCreateCommandPool(device, &poolInfo, nullptr, &objects.commandPool) != VK_SUCCESS)
|
||||
return fail("vkCreateCommandPool failed");
|
||||
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
commandInfo.commandPool = objects.commandPool;
|
||||
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
commandInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS)
|
||||
return fail("vkAllocateCommandBuffers failed");
|
||||
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed");
|
||||
|
||||
const VkRect2D fullRect{{0, 0}, {kProbeExtent, kProbeExtent}};
|
||||
// (1) Every image UNDEFINED -> DEPTH_STENCIL_ATTACHMENT_OPTIMAL, the way TransitionWireImage
|
||||
// moves a fresh image (source state from UNDEFINED, destination ALL_COMMANDS).
|
||||
for (const FormatWork& item : work) {
|
||||
const VkImage images[] = {item.subjectSource.image, item.controlSource.image, item.subjectTarget.image,
|
||||
item.depthControl.ready ? item.depthControl.target.image : VK_NULL_HANDLE,
|
||||
item.stencilControl.ready ? item.stencilControl.target.image : VK_NULL_HANDLE};
|
||||
for (const VkImage image : images) {
|
||||
if (image == VK_NULL_HANDLE) continue;
|
||||
ImageBarrier(cmd, image, item.aspects, VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
}
|
||||
}
|
||||
// (2) The inputs and the sentinels, cleared the wire's way.
|
||||
for (const ClearPass& clear : clears) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = clear.pass;
|
||||
begin.framebuffer = clear.framebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
VkClearAttachment attachment{};
|
||||
attachment.aspectMask = clear.aspects;
|
||||
attachment.clearValue.depthStencil = {clear.depth, clear.stencil};
|
||||
const VkClearRect rect{fullRect, 0, 1};
|
||||
vkCmdClearAttachments(cmd, 1, &attachment, 1, &rect);
|
||||
vkCmdEndRenderPass(cmd);
|
||||
}
|
||||
// (3) What TransitionWireImage records when the layout already matches - the case of a
|
||||
// source the application cleared and then resolves.
|
||||
{
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
|
||||
&memory, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
// (4) THE SUBJECT: the render-pass arm, begun and ended with no draw. Under the test-only
|
||||
// elideSubject it is not recorded at all: its target keeps the sentinel cleared in (2) and
|
||||
// the layout it was left in, which is what (6) expects either way.
|
||||
if (!context.elideSubject) {
|
||||
for (const FormatWork& item : work) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = item.resolvePass;
|
||||
begin.framebuffer = item.resolveFramebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(cmd);
|
||||
}
|
||||
}
|
||||
// (5) THE CONTROL: the shader arm on the twin source, which TransitionWireImage moves to
|
||||
// SHADER_READ_ONLY_OPTIMAL first (both aspects of a packed format).
|
||||
for (const FormatWork& item : work) {
|
||||
if (!item.depthControl.ready && !item.stencilControl.ready) continue;
|
||||
ImageBarrier(cmd, item.controlSource.image, item.aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
for (const ControlPass* control : {&item.depthControl, &item.stencilControl}) {
|
||||
if (!control->ready) continue;
|
||||
RecordWireShaderResolveDraw(cmd, objects.kit, control->pass, control->pipeline, control->framebuffer,
|
||||
control->set, {kProbeExtent, kProbeExtent}, fullRect);
|
||||
}
|
||||
}
|
||||
// (6) Every target to TRANSFER_SRC with the barrier ResolveWireDepthStencil puts after its
|
||||
// resolve (a depth/stencil resolve writes in COLOR_ATTACHMENT_OUTPUT), then one copy per aspect.
|
||||
const auto copyOut = [&](VkImage image, VkImageAspectFlags aspect, VkDeviceSize offset) {
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = offset;
|
||||
copy.imageSubresource = {aspect, 0, 0, 1};
|
||||
copy.imageExtent = {kProbeExtent, kProbeExtent, 1};
|
||||
vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, objects.buffer, 1, ©);
|
||||
};
|
||||
const auto toTransfer = [&](VkImage image, VkImageAspectFlags aspects) {
|
||||
ImageBarrier(cmd, image, aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
};
|
||||
for (const FormatWork& item : work) {
|
||||
toTransfer(item.subjectTarget.image, item.aspects);
|
||||
if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT)
|
||||
copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.subjectDepthOffset);
|
||||
if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT)
|
||||
copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.subjectStencilOffset);
|
||||
if (item.depthControl.ready) {
|
||||
toTransfer(item.depthControl.target.image, item.aspects);
|
||||
copyOut(item.depthControl.target.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl.offset);
|
||||
}
|
||||
if (item.stencilControl.ready) {
|
||||
toTransfer(item.stencilControl.target.image, item.aspects);
|
||||
copyOut(item.stencilControl.target.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl.offset);
|
||||
}
|
||||
}
|
||||
{
|
||||
VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.buffer = objects.buffer;
|
||||
barrier.size = VK_WHOLE_SIZE;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1,
|
||||
&barrier, 0, nullptr);
|
||||
}
|
||||
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed");
|
||||
|
||||
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS)
|
||||
return fail("vkCreateFence failed");
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &cmd;
|
||||
if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS)
|
||||
return fail(format("vkQueueSubmit failed (VkResult {})", static_cast<Int>(result)));
|
||||
const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs);
|
||||
if (waited == VK_TIMEOUT) {
|
||||
// The submission may still be executing: destroying anything it names, or idle-waiting
|
||||
// the device, is the hang the bound exists to prevent. Everything leaks.
|
||||
leak = true;
|
||||
measurement.fenceWaitTimedOut = true;
|
||||
return fail("the probe's fence did not signal within 5 s");
|
||||
}
|
||||
if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast<Int>(waited)));
|
||||
|
||||
void* mapped = nullptr;
|
||||
if (vkMapMemory(device, bufferMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr)
|
||||
return fail("vkMapMemory failed");
|
||||
if (!bufferCoherent) {
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = bufferMemory;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
vkInvalidateMappedMemoryRanges(device, 1, &range);
|
||||
}
|
||||
const Uint8* bytes = static_cast<const Uint8*>(mapped);
|
||||
const auto tally = [&](WireDepthResolveAspectReading& aspect, VkDeviceSize offset, Uint32 texelBytes,
|
||||
Bool depth, VkFormat format, Bool subject) {
|
||||
Uint32 matches = 0, sentinels = 0, first = 0;
|
||||
for (Uint32 i = 0; i < kProbeTexels; ++i) {
|
||||
const Uint8* texel = bytes + offset + static_cast<VkDeviceSize>(i) * texelBytes;
|
||||
const Uint32 word = depth ? ReadDepthWord(format, texel) : static_cast<Uint32>(*texel);
|
||||
if (i == 0) first = word;
|
||||
if (WordDistance(word, aspect.expected) <= aspect.tolerance) ++matches;
|
||||
if (WordDistance(word, aspect.sentinel) <= aspect.tolerance) ++sentinels;
|
||||
}
|
||||
if (subject) {
|
||||
aspect.renderPassMatches = matches;
|
||||
aspect.renderPassSentinels = sentinels;
|
||||
aspect.renderPassFirst = first;
|
||||
} else {
|
||||
aspect.shaderRan = true;
|
||||
aspect.shaderMatches = matches;
|
||||
aspect.shaderFirst = first;
|
||||
}
|
||||
};
|
||||
for (const FormatWork& item : work) {
|
||||
WireDepthResolveFormatReading& reading = measurement.formats[item.reading];
|
||||
if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
auto& aspect = reading.depth;
|
||||
aspect.measured = true;
|
||||
aspect.texels = kProbeTexels;
|
||||
aspect.expected = EncodeDepth(item.format, kClearDepth);
|
||||
aspect.sentinel = EncodeDepth(item.format, kSentinelDepth);
|
||||
aspect.tolerance = IsFloatDepth(item.format) ? 0u : 1u;
|
||||
tally(aspect, item.subjectDepthOffset, DepthCopyBytes(item.format), true, item.format, true);
|
||||
if (item.depthControl.ready)
|
||||
tally(aspect, item.depthControl.offset, DepthCopyBytes(item.format), true, item.format, false);
|
||||
}
|
||||
if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
||||
auto& aspect = reading.stencil;
|
||||
aspect.measured = true;
|
||||
aspect.texels = kProbeTexels;
|
||||
aspect.expected = kClearStencil;
|
||||
aspect.sentinel = kSentinelStencil;
|
||||
aspect.tolerance = 0;
|
||||
tally(aspect, item.subjectStencilOffset, 1, false, item.format, true);
|
||||
if (item.stencilControl.ready)
|
||||
tally(aspect, item.stencilControl.offset, 1, false, item.format, false);
|
||||
}
|
||||
}
|
||||
vkUnmapMemory(device, bufferMemory);
|
||||
measurement.ran = true;
|
||||
if (!kitFailure.empty()) measurement.failureReason = kitFailure;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice(
|
||||
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
Bool& renderPassArmAvailable, Bool& shaderStencilExport) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
const auto has = [&](const char* name) {
|
||||
for (const VkExtensionProperties& extension : deviceExtensions)
|
||||
if (std::strcmp(extension.extensionName, name) == 0) return true;
|
||||
return false;
|
||||
};
|
||||
renderPassArmAvailable = has(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) &&
|
||||
has(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME);
|
||||
shaderStencilExport = has(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
if (!renderPassArmAvailable) {
|
||||
measurement.failureReason = "no VK_KHR_create_renderpass2 + VK_KHR_depth_stencil_resolve";
|
||||
return measurement;
|
||||
}
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
|
||||
auto getProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2"));
|
||||
if (getProperties2 == nullptr)
|
||||
getProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2KHR"));
|
||||
const auto createDevice =
|
||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||
if (properties.apiVersion < VK_API_VERSION_1_1 || getProperties2 == nullptr || createDevice == nullptr) {
|
||||
renderPassArmAvailable = false;
|
||||
measurement.failureReason = "a pre-1.1 device, or no vkGetPhysicalDeviceProperties2 / vkCreateDevice";
|
||||
return measurement;
|
||||
}
|
||||
VkPhysicalDeviceDepthStencilResolveProperties resolveProperties{
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 properties2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
||||
properties2.pNext = &resolveProperties;
|
||||
getProperties2(physicalDevice, &properties2);
|
||||
|
||||
// The renderer's own extension set for the two arms (VulkanRenderer.cpp,
|
||||
// CreateLogicalDeviceAndQueues), nothing more.
|
||||
const Float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
queueInfo.queueCount = 1;
|
||||
queueInfo.pQueuePriorities = &priority;
|
||||
Vector<const char*> extensions{VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME,
|
||||
VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME};
|
||||
if (shaderStencilExport) extensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||
deviceInfo.enabledExtensionCount = static_cast<Uint32>(extensions.size());
|
||||
deviceInfo.ppEnabledExtensionNames = extensions.data();
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device);
|
||||
result != VK_SUCCESS || device == VK_NULL_HANDLE) {
|
||||
measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast<Int>(result));
|
||||
return measurement;
|
||||
}
|
||||
WireDepthResolveProbeContext context;
|
||||
context.physicalDevice = physicalDevice;
|
||||
context.device = device;
|
||||
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue);
|
||||
context.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
context.createRenderPass2 =
|
||||
reinterpret_cast<PFN_vkCreateRenderPass2>(vkGetDeviceProcAddr(device, "vkCreateRenderPass2KHR"));
|
||||
context.depthResolveModes = resolveProperties.supportedDepthResolveModes;
|
||||
context.stencilResolveModes = resolveProperties.supportedStencilResolveModes;
|
||||
context.shaderStencilExport = shaderStencilExport;
|
||||
measurement = RunWireDepthResolveProbe(context);
|
||||
// A timed-out probe leaked its children on purpose; destroying their device would block
|
||||
// exactly where the bound says not to.
|
||||
if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr);
|
||||
return measurement;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,121 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
#pragma once
|
||||
|
||||
#include "WireDepthResolveArm.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <Includes.h>
|
||||
|
||||
// P7 gate 5 (g5-msprobe): the Vulkan half of the multisample depth/stencil resolve probe, and the
|
||||
// two resolve arms it measures.
|
||||
//
|
||||
// THE ARMS LIVE HERE, NOT ONLY IN THE RENDERER. A probe that rebuilt "something like" the wire's
|
||||
// render pass would measure its own copy; the render-pass description, the shader pass's modules,
|
||||
// pipeline and draw, and the view shapes both arms attach are defined once below and called by
|
||||
// ResolveWireDepthStencil (WireFramebuffer.inc), ResolveWireDepthStencilWithShader
|
||||
// (WireMultisampleResolve.inc) and the probe alike.
|
||||
//
|
||||
// Two callers: the Magma renderer at device bring-up (server side, once per process, only where
|
||||
// the render-pass arm exists at all - VulkanRenderer::ArmWireDepthResolveOrder) and the POST's
|
||||
// "Known Driver Bugs" section (MG_Util/SelfTest/DriverBugProbes.cpp) on a throwaway device.
|
||||
//
|
||||
// Disaggregated builds only: the monolith never records either arm, and the pull build's image
|
||||
// must not change (P7 gate G1).
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Every aspect a wire depth/stencil format has.
|
||||
VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format);
|
||||
|
||||
// A 2D view of one level/layer of a depth/stencil image naming `aspects`. The render-pass arm
|
||||
// attaches both images with every aspect of the format; the shader arm SAMPLES its source
|
||||
// through a view of exactly one aspect (a depth/stencil image can only be sampled that way) and
|
||||
// attaches its target with every aspect.
|
||||
VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects,
|
||||
Uint32 level, Uint32 layer);
|
||||
|
||||
// THE RENDER-PASS ARM: attachment 0 the multisample source (LOAD/STORE), attachment 1 the
|
||||
// single-sample resolve target (DONT_CARE/STORE), both in DEPTH_STENCIL_ATTACHMENT_OPTIMAL, one
|
||||
// subpass whose VkSubpassDescriptionDepthStencilResolve resolves every aspect of the format
|
||||
// with VK_RESOLVE_MODE_SAMPLE_ZERO_BIT. The caller begins and ends it with NO draw.
|
||||
VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device,
|
||||
VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass);
|
||||
|
||||
// THE SHADER ARM's device objects that do not depend on the format.
|
||||
struct WireShaderResolveKit {
|
||||
VkShaderModule vertex = VK_NULL_HANDLE;
|
||||
VkShaderModule depthFragment = VK_NULL_HANDLE;
|
||||
// Only where VK_EXT_shader_stencil_export is enabled: its SPIR-V declares StencilExportEXT.
|
||||
VkShaderModule stencilFragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE;
|
||||
};
|
||||
// Creates every member; the first failure is returned and the members made so far stay set,
|
||||
// for DestroyWireShaderResolveKit.
|
||||
VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit);
|
||||
void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit);
|
||||
// One (format, aspect)'s single-attachment render pass: DONT_CARE/STORE on both aspects,
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL throughout.
|
||||
VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass);
|
||||
// Its pipeline: full-screen triangle, gl_FragDepth (ALWAYS + write) or gl_FragStencilRefARB
|
||||
// (REPLACE everywhere), viewport and scissor dynamic. VK_ERROR_FEATURE_NOT_PRESENT when the kit
|
||||
// has no fragment module for `aspect`.
|
||||
VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass,
|
||||
VkImageAspectFlags aspect, VkPipeline* pipeline);
|
||||
// Records the draw: the render area and the scissor are `rect`, the viewport is the WHOLE
|
||||
// attachment (gl_FragCoord is what the fragment stage fetches by), one triangle.
|
||||
void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit,
|
||||
VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer,
|
||||
VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect);
|
||||
|
||||
// What the probe runs on. The device must have been created with VK_KHR_create_renderpass2 and
|
||||
// VK_KHR_depth_stencil_resolve (or Vulkan 1.2 core), and with VK_EXT_shader_stencil_export where
|
||||
// `shaderStencilExport` says so. The queue must be otherwise idle for the probe's duration.
|
||||
struct WireDepthResolveProbeContext {
|
||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
Uint32 queueFamilyIndex = 0;
|
||||
PFN_vkCreateRenderPass2 createRenderPass2 = nullptr;
|
||||
VkResolveModeFlags depthResolveModes = 0;
|
||||
VkResolveModeFlags stencilResolveModes = 0;
|
||||
Bool shaderStencilExport = false;
|
||||
// TEST ONLY (MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject, WireDepthResolveArm.h): record
|
||||
// NO render-pass resolve, so every subject target keeps its sentinel - the Adreno 830's
|
||||
// reading - while the shader control runs as always. Everything else is the real probe.
|
||||
Bool elideSubject = false;
|
||||
};
|
||||
|
||||
// Per format the device can host as a 4x multisample depth/stencil attachment AND for which
|
||||
// ResolveWireDepthStencil would take the render-pass arm (D16, X8_D24, D32F, D24S8, D32FS8, S8):
|
||||
// - two identical 4x4 4x multisample sources (production usage), each cleared to depth 0.25 /
|
||||
// stencil 0x5A the way the wire clears (a LOAD pass with vkCmdClearAttachments);
|
||||
// - single-sample resolve targets (the production scratch usage) pre-filled with the sentinel
|
||||
// depth 0.75 / stencil 0xA5;
|
||||
// - SUBJECT: the render-pass arm's exact pass, begun and ended with no draw;
|
||||
// - CONTROL: the shader arm's exact pass on the other source, one target per aspect (stencil
|
||||
// only with VK_EXT_shader_stencil_export);
|
||||
// - every target copied aspect by aspect into a host-visible buffer and compared texel by texel.
|
||||
// One submission, a bounded fence wait (on timeout every object is leaked and the measurement
|
||||
// says so - the caller must then not destroy or idle-wait the device), teardown on every other
|
||||
// path. Never touches renderer state.
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context);
|
||||
|
||||
// The POST's entry: creates a throwaway device on `physicalDevice` with exactly the extensions
|
||||
// the renderer enables for the two arms, runs the probe on its graphics queue and destroys the
|
||||
// device (leaks it on a fence timeout). `renderPassArmAvailable` is false - and nothing is run -
|
||||
// when the device lacks VK_KHR_create_renderpass2 / VK_KHR_depth_stencil_resolve; the shader
|
||||
// pass is then the only arm and the probe has nothing to choose between.
|
||||
// `shaderStencilExport` reports whether VK_EXT_shader_stencil_export was available.
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice(
|
||||
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
Bool& renderPassArmAvailable, Bool& shaderStencilExport);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,631 +0,0 @@
|
||||
// Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) {
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const Bool composite = handle.Slot >= MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
|
||||
const auto& records = composite ? state.CompositeShaderCsos : state.ShaderCsos;
|
||||
const SizeT index = composite ? handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase : handle.Slot;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle) || index >= records.size() ||
|
||||
!records[index].Live || records[index].Gen != handle.Gen || !records[index].Archive)
|
||||
MagmaWireFatal("program-record");
|
||||
return MagmaProgramSource(handle, records[index]);
|
||||
}
|
||||
|
||||
// Through the same seam as MagmaWireFatal, and for the same reason (P7 wave 0). The family
|
||||
// stays RoleViolation: reaching the legacy buffer arm inside a Magma server is a role
|
||||
// invariant the two halves must hold, not a verb the wire failed to migrate.
|
||||
[[noreturn]] static void WireBufferLegacyFatal() {
|
||||
MG_Pipe::MGPipeSessionFail(
|
||||
MG_Pipe::MGPipeFatalFamily::RoleViolation,
|
||||
"MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)");
|
||||
}
|
||||
|
||||
static const MG_Pipe::MGPStreamOutputBegin& WireXfbSpan() {
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId);
|
||||
if (found == state.StreamOutputSpans.end()) MagmaWireFatal("xfb-span-record");
|
||||
return found->second;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BeginWireXfbCaptureForDraw(FrameContext::FrameData& frame) {
|
||||
const auto& span = WireXfbSpan();
|
||||
const auto program = WireProgramSource(span.CaptureProgram);
|
||||
const Uint32 count = static_cast<Uint32>(std::min<SizeT>(program.GetTransformFeedbackBufferCount(), 4));
|
||||
if (!count || !program.GetTransformFeedbackVaryingCount()) return false;
|
||||
VkBuffer buffers[4]{};
|
||||
VkDeviceSize offsets[4]{};
|
||||
VkDeviceSize sizes[4]{};
|
||||
Bool hasTarget = false;
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
// A leading gl_NextBuffer leaves a hole in the binding indices. No
|
||||
// output names that zero-stride target, and GL need not bind a buffer.
|
||||
if (!program.GetTransformFeedbackStride(i)) continue;
|
||||
const auto& target = span.Targets[i];
|
||||
BufferSlice slice{};
|
||||
if (MG_Pipe::MGPipeHandleIsNull(target.Res) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, target.Res, slice))
|
||||
MagmaWireFatal("xfb-buffer-resource");
|
||||
if (target.Offset >= slice.size) return false;
|
||||
const Uint64 size = target.Size == 0 || target.Size == MG_Pipe::kMGPipeWholeBuffer
|
||||
? slice.size - target.Offset : target.Size;
|
||||
if (!size || size > slice.size - target.Offset) return false;
|
||||
buffers[i] = slice.buffer;
|
||||
offsets[i] = slice.offset + target.Offset;
|
||||
sizes[i] = size;
|
||||
hasTarget = true;
|
||||
// The server owns the storage. Publish dirtiness through the existing
|
||||
// reverse channel so CPU readback does not reuse the client's old shadow.
|
||||
m_bufferManager.MarkWireBufferGpuWritten(target.Res, target.Offset, size);
|
||||
}
|
||||
if (!hasTarget) return false;
|
||||
return BeginXfbCaptureWithBuffers(frame, count, buffers, offsets, sizes);
|
||||
}
|
||||
|
||||
static Bool ConvertWireVertexStream(const MGPVertexAttribWire& attr,
|
||||
VertexInputStateFactory::VertexStreamConversion conversion, const Uint8* source,
|
||||
SizeT stride, SizeT count, Vector<Uint8>& output) {
|
||||
using Conversion = VertexInputStateFactory::VertexStreamConversion;
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
const SizeT elementBytes = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (conversion == Conversion::Repack) {
|
||||
output.resize(count * elementBytes);
|
||||
for (SizeT i = 0; i < count; ++i)
|
||||
Memcpy(output.data() + i * elementBytes, source + i * stride, elementBytes);
|
||||
return true;
|
||||
}
|
||||
output.resize(count * attr.Size * sizeof(Float));
|
||||
const auto convert = [&]<typename T>() {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
for (SizeT component = 0; component < attr.Size; ++component) {
|
||||
T value{};
|
||||
Memcpy(&value, source + i * stride + component * sizeof(T), sizeof(T));
|
||||
const Float converted = static_cast<Float>(value);
|
||||
Memcpy(output.data() + (i * attr.Size + component) * sizeof(Float), &converted, sizeof(Float));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
switch (type) {
|
||||
case DataType::Float64: return convert.template operator()<Double>();
|
||||
case DataType::Int8: return convert.template operator()<Int8>();
|
||||
case DataType::Uint8: return convert.template operator()<Uint8>();
|
||||
case DataType::Int16: return convert.template operator()<Int16>();
|
||||
case DataType::Uint16: return convert.template operator()<Uint16>();
|
||||
case DataType::Int32: return convert.template operator()<Int32>();
|
||||
case DataType::Uint32: return convert.template operator()<Uint32>();
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::DestroyWireDrawPass() {
|
||||
// Only shutdown/swapchain replacement calls this, after device idle.
|
||||
if (m_wireDrawPass && m_frameContext.GetFrameCount() != 0) {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
m_wireDrawPass.reset();
|
||||
for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr);
|
||||
m_wireDrawViews.clear();
|
||||
m_wireDrawPassKey = {};
|
||||
}
|
||||
|
||||
void VulkanRenderer::RetireWireObjects(WireRetiredObjects objects) {
|
||||
// The current recording is the next graphics submission. Do not use a
|
||||
// frame number: mid-frame flushes and readbacks can split one frame.
|
||||
objects.submitIndex = m_submitCounter + 1;
|
||||
m_wireRetiredObjects.push_back(Move(objects));
|
||||
}
|
||||
|
||||
void VulkanRenderer::RetireWireDrawPass() {
|
||||
if (!m_wireDrawPass) return;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
if (m_wireDrawPassCaches.size() < m_frameContext.GetFrameCount())
|
||||
m_wireDrawPassCaches.resize(m_frameContext.GetFrameCount());
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
// Bound a present-less stream of distinct framebuffer configurations.
|
||||
// Overflow retains the original submit-index retirement path.
|
||||
constexpr SizeT kMaxCachedWirePassesPerFrame = 32;
|
||||
if (cache.size() < kMaxCachedWirePassesPerFrame) {
|
||||
cache.push_back({Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews)});
|
||||
return;
|
||||
}
|
||||
WireRetiredObjects retired{};
|
||||
retired.drawPass = Move(m_wireDrawPass);
|
||||
retired.imageViews = Move(m_wireDrawViews);
|
||||
RetireWireObjects(Move(retired));
|
||||
m_wireDrawPassKey = {};
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::AcquireCachedWireDrawPass(const WireDrawPassKey& key) {
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
if (frameIndex >= m_wireDrawPassCaches.size()) return false;
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
for (auto it = cache.begin(); it != cache.end(); ++it) {
|
||||
// Exact values, not a compatibility hash: two FBOs with the same
|
||||
// formats can name different images, windows or draw-buffer slots.
|
||||
if (!(it->key == key)) continue;
|
||||
m_wireDrawPassKey = Move(it->key);
|
||||
m_wireDrawPass = Move(it->pass);
|
||||
m_wireDrawViews = Move(it->views);
|
||||
cache.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) {
|
||||
if (frameIndex >= m_wireDrawPassCaches.size()) return;
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
for (auto& entry : cache) {
|
||||
entry.pass.reset();
|
||||
for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr);
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearAllWireDrawPassCaches() {
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_wireDrawPassCaches.size(); ++frameIndex)
|
||||
ClearWireDrawPassCache(frameIndex);
|
||||
}
|
||||
|
||||
void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) {
|
||||
SizeT retained = 0;
|
||||
for (SizeT index = 0; index < m_wireRetiredObjects.size(); ++index) {
|
||||
auto& entry = m_wireRetiredObjects[index];
|
||||
if (!all && entry.submitIndex > completedSubmit) {
|
||||
if (retained != index) m_wireRetiredObjects[retained] = Move(entry);
|
||||
++retained;
|
||||
continue;
|
||||
}
|
||||
// Framebuffers/descriptor sets stop naming their views first.
|
||||
entry.drawPass.reset();
|
||||
for (const auto pool : entry.descriptorPools) vkDestroyDescriptorPool(m_device, pool, nullptr);
|
||||
for (const auto framebuffer : entry.framebuffers) vkDestroyFramebuffer(m_device, framebuffer, nullptr);
|
||||
for (const auto pass : entry.renderPasses) vkDestroyRenderPass(m_device, pass, nullptr);
|
||||
for (const auto view : entry.imageViews) vkDestroyImageView(m_device, view, nullptr);
|
||||
}
|
||||
// Erase once: repeated front erases move the whole remaining cohort for
|
||||
// every draw retired by a fence, turning a batch retirement quadratic.
|
||||
m_wireRetiredObjects.resize(retained);
|
||||
}
|
||||
|
||||
struct WirePreparationScope {
|
||||
Uint32& depth;
|
||||
explicit WirePreparationScope(Uint32& value) : depth(value) { ++depth; }
|
||||
~WirePreparationScope() { --depth; }
|
||||
};
|
||||
|
||||
Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode,
|
||||
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) {
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto program = WireProgramSource(state.BoundShaderCso);
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
|
||||
return false;
|
||||
}
|
||||
if (aspects & DrawSetupAspect::IndirectDrawBuffer)
|
||||
WireBufferLegacyFatal();
|
||||
MakeXfbWritesVisible();
|
||||
const auto* fbo = state.DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("draw-framebuffer-record");
|
||||
RetireWireDrawPass();
|
||||
// CPU preparation may synchronize GPU-produced vertex/index bytes. Do
|
||||
// not rewind an arena while this draw owns as-yet-unrecorded slices.
|
||||
WirePreparationScope preparing(m_wirePreparationDepth);
|
||||
|
||||
ProgramFactory::CompileOptionFlags flags =
|
||||
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(fbo->IsDefault));
|
||||
if (m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0)
|
||||
flags |= ProgramFactory::CompileOptionBit::XfbCapture;
|
||||
const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
if (!(aspects & DrawSetupAspect::IndexBuffer) && programObject->readsBaseVertexBuiltin) {
|
||||
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
}
|
||||
if (programObject->pointSizeCapabilityUnsupported) {
|
||||
MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it");
|
||||
return false;
|
||||
}
|
||||
const auto& programObj = *programObject;
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(TextureResourcesUnprepared, "a sampled/storage image could not be made ready; see the Tex* tally");
|
||||
return false;
|
||||
}
|
||||
// Lazy image uploads/promotions can submit the old recording. Resolve
|
||||
// ALL attachments before descriptor binding captures the current cmd.
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
||||
const Int32 slot = fbo->DrawBuffers[i];
|
||||
if (slot >= 0) (void)ResolveWireImage(*fbo, fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
}
|
||||
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
(void)ResolveWireImage(*fbo, fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
(void)ResolveWireImage(*fbo, fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
m_textureManager->FlushPendingUploads();
|
||||
const auto vaoHandle = state.BoundVertexElements;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record");
|
||||
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
|
||||
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
|
||||
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
|
||||
VertexInputStateFactory::BackendVertexInputState input{};
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT.
|
||||
//
|
||||
// A false is now ONE reason and only one - `buffer-window`, an enabled attribute the
|
||||
// program reads whose slot lies outside the window set_vertex_buffers published. That
|
||||
// is a disagreement about what crossed, not about what this device can express, so it
|
||||
// keeps a named Fatal through the hook (MGPipeSessionFail -> Session::Fail, which is
|
||||
// what rule I asks of a refusal that must stay a refusal). It is the check that caught
|
||||
// the Redmi VAO hash collision fixed in 376c04be, which is why it is not softened.
|
||||
// The P7 marker is gone from the detail string with the abort: this is a permanent
|
||||
// protocol error, not a shape the split path has yet to migrate, so it must not keep
|
||||
// counting against the census of unmigrated refusals (scripts/ci/fatal_census.py
|
||||
// counts the marker literally, which is also why no comment in this package spells it).
|
||||
if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state,
|
||||
programObj.activeVertexInputLocationMask, input))
|
||||
MagmaWireFatal("vertex-layout-buffer-window");
|
||||
// The seven device/format reasons are now MASKED OUT of the vertex input state, and
|
||||
// this is the monolith arm's pre-flight for them, statement for statement
|
||||
// (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An
|
||||
// enabled array with no usable VkFormat never reaches the vertex input state, which
|
||||
// makes it indistinguishable from a DISABLED one - and a disabled one is answered
|
||||
// below by uploading the current attribute value, so without this check the draw would
|
||||
// quietly feed the shader a constant in place of the application's vertex data.
|
||||
//
|
||||
// A DECLINE, NOT A FATAL (rule I (a)): the draw is skipped, the reason is named once,
|
||||
// and the observable is the monolith lane's for the same VAO on the same device.
|
||||
// BuildWireVertexInput only considers attributes the program reads, so a masked bit is
|
||||
// always an active one and this predicate is the whole of the difference.
|
||||
if ((input.unsupportedAttribMask & programObj.activeVertexInputLocationMask) != 0) {
|
||||
MGLOG_E_ONCE("Magma wire draw skipped: program=%u reads vertex attribute location "
|
||||
"mask 0x%x whose enabled array has no supported vertex format",
|
||||
program.GetExternalIndex(),
|
||||
input.unsupportedAttribMask & programObj.activeVertexInputLocationMask);
|
||||
WireDeclineTally::Count(WireDeclineSite::VertexFormatUnsupported);
|
||||
return false;
|
||||
}
|
||||
Vector<BufferSlice> vertexSlices(input.bindings.size());
|
||||
for (SizeT binding = 0; binding < input.bindings.size(); ++binding) {
|
||||
const Uint32 location = input.bindingAttributeLocations[binding];
|
||||
const auto& attr = vao.Attributes[location];
|
||||
const auto& resource = state.VertexBuffers[location];
|
||||
if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8");
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
|
||||
MagmaWireFatal("vertex-buffer-resource");
|
||||
const VkDeviceSize offset = input.bindingBaseOffsets[binding];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (offset > slice.size || elementSize > slice.size - offset) {
|
||||
MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's first element lies outside its buffer");
|
||||
return false;
|
||||
}
|
||||
const auto conversion = input.bindingConversions[binding];
|
||||
if (conversion != VertexInputStateFactory::VertexStreamConversion::None) {
|
||||
const SizeT stride = static_cast<SizeT>(attr.Stride);
|
||||
SizeT count = stride ? 1 + (static_cast<SizeT>(slice.size - offset) - elementSize) / stride : 1;
|
||||
// Direct non-indexed draws bound the conversion; indexed/multi-draw may
|
||||
// name arbitrary vertices, so preserve the entire valid source stream.
|
||||
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor)
|
||||
count = std::min<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
|
||||
if (!count) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> source((count - 1) * stride + elementSize);
|
||||
if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamRead, "the vertex bytes to convert could not be read back");
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> converted;
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-format-conversion`: DECLINE.
|
||||
// The converter is asked only for a conversion BuildWireVertexInput already
|
||||
// chose, so a refusal here means the two disagree about what is convertible -
|
||||
// a MobileGL-internal defect, not a peer's malformed record and not a verb the
|
||||
// split path has yet to migrate. Killing the session for it told the peer
|
||||
// "protocol corruption" about a bug on this side; skipping the draw and naming
|
||||
// it once is the honest answer and is what the row asks for. The packed
|
||||
// converter that would make the refusal unreachable is the row's optional half
|
||||
// and is not taken here.
|
||||
if (!ConvertWireVertexStream(attr, conversion, source.data(), stride, count, converted)) {
|
||||
MGLOG_E_ONCE("Magma wire draw skipped: no vertex stream converter for "
|
||||
"location=%u type=%u size=%u conversion=%d",
|
||||
location, attr.Type, static_cast<Uint32>(attr.Size),
|
||||
static_cast<Int>(conversion));
|
||||
WireDeclineTally::Count(WireDeclineSite::VertexStreamConverter);
|
||||
return false;
|
||||
}
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
converted.data(), converted.size(), 16, vertexSlices[binding])) {
|
||||
MGL_WIRE_DECLINE_AT(VertexTransientUpload, "the converted vertex stream did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
slice.offset += offset;
|
||||
slice.size -= offset;
|
||||
vertexSlices[binding] = slice;
|
||||
}
|
||||
}
|
||||
|
||||
BufferSlice indexSlice{};
|
||||
VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM;
|
||||
if (aspects & DrawSetupAspect::IndexBuffer) {
|
||||
if (!indices) {
|
||||
MGL_WIRE_DECLINE_AT(IndexViewMissing, "an indexed draw arrived with no index view");
|
||||
return false;
|
||||
}
|
||||
const SizeT width = MG_Util::GetGLTypeSize(indices->indexType);
|
||||
if (width != 1 && width != 2 && width != 4) {
|
||||
MGL_WIRE_DECLINE_AT(IndexTypeWidth, "the index type is not 1, 2 or 4 bytes wide");
|
||||
return false;
|
||||
}
|
||||
indexType = width == 4 ? VK_INDEX_TYPE_UINT32 : width == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8;
|
||||
const auto& restart = MGB_CTX->GetRenderStateParameters();
|
||||
const Uint32 fixed = MG_Util::FixedRestartIndexForGLType(indices->indexType);
|
||||
const Bool replaceRestart = restart.PrimitiveRestartEnabled && !restart.PrimitiveRestartFixedIndexEnabled &&
|
||||
restart.PrimitiveRestartIndex < fixed;
|
||||
const Bool expandByte = width == 1 && !m_indexTypeUint8ExtensionEnabled;
|
||||
const Bool cpuIndices = indices->forceClientMemory || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res);
|
||||
Vector<Uint8> copied;
|
||||
const void* bytes = nullptr;
|
||||
if (cpuIndices) bytes = reinterpret_cast<const void*>(indices->indexByteOffset);
|
||||
else {
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice))
|
||||
MagmaWireFatal("index-buffer-resource");
|
||||
const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset;
|
||||
if (offset < state.IndexBuffer.Offset || offset > indexSlice.size ||
|
||||
indices->indexByteSize > indexSlice.size - offset) {
|
||||
MGL_WIRE_DECLINE_AT(IndexWindow, "the index range lies outside its buffer");
|
||||
return false;
|
||||
}
|
||||
if (replaceRestart || expandByte || offset % width) {
|
||||
copied.resize(indices->indexByteSize);
|
||||
if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) {
|
||||
MGL_WIRE_DECLINE_AT(IndexStreamRead, "the indices to rewrite could not be read back");
|
||||
return false;
|
||||
}
|
||||
bytes = copied.data();
|
||||
}
|
||||
indexSlice.offset += offset;
|
||||
indexSlice.size = indices->indexByteSize;
|
||||
}
|
||||
if (bytes) {
|
||||
Vector<Uint8> rewritten;
|
||||
SizeT uploadSize = indices->indexByteSize;
|
||||
if (replaceRestart || expandByte) {
|
||||
const SizeT outWidth = expandByte ? 2 : width;
|
||||
rewritten.resize(uploadSize / width * outWidth);
|
||||
for (SizeT i = 0; i < uploadSize / width; ++i) {
|
||||
Uint32 value = 0;
|
||||
Memcpy(&value, static_cast<const Uint8*>(bytes) + i * width, width);
|
||||
if (replaceRestart && value == restart.PrimitiveRestartIndex) value = fixed;
|
||||
if (expandByte && ResolvePrimitiveRestartEnable(aspects, indices) && value == fixed) value = 0xffffu;
|
||||
Memcpy(rewritten.data() + i * outWidth, &value, outWidth);
|
||||
}
|
||||
bytes = rewritten.data();
|
||||
uploadSize = rewritten.size();
|
||||
if (expandByte) indexType = VK_INDEX_TYPE_UINT16;
|
||||
}
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
|
||||
bytes, uploadSize, 16, indexSlice)) {
|
||||
MGL_WIRE_DECLINE_AT(IndexTransientUpload, "the rewritten indices did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!indexSlice.IsValid()) {
|
||||
MGL_WIRE_DECLINE_AT(IndexSliceInvalid, "no usable index slice was resolved");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
// Descriptor resolution can upgrade a view's root image to STORAGE usage.
|
||||
// Resolve it before constructing framebuffer views so both roles of that image
|
||||
// name the same allocation even on its first storage-image use.
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) {
|
||||
MGL_WIRE_DECLINE_AT(UniformBufferBinding, "the program's uniform/storage bindings could not be bound");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<VkAttachmentDescription> attachments;
|
||||
Vector<VkImageViewCreateInfo> viewInfos;
|
||||
Vector<VkAttachmentReference> colors;
|
||||
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
|
||||
VkExtent2D extent{~0u, ~0u};
|
||||
Uint32 layers = ~0u;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
Uint64 passHash = 0;
|
||||
WireDrawPassKey passKey{};
|
||||
passKey.isDefault = fbo->IsDefault;
|
||||
passKey.framebufferSrgb = MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
||||
if (fbo->IsDefault) {
|
||||
passKey.swapchain = m_swapchainObject.GetHandle();
|
||||
// A drawing pass on the default framebuffer is what makes the freshly acquired
|
||||
// swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the
|
||||
// attachments below resolve the same index, so this key has to be taken after it.
|
||||
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
|
||||
}
|
||||
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
|
||||
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
|
||||
if (!image.image) return VK_ATTACHMENT_UNUSED;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT)
|
||||
image.format = ResolveSrgbAttachmentWriteFormat(image.format,
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
|
||||
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
|
||||
TransitionWireImage(image, layout);
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = image.image;
|
||||
info.viewType = image.viewType;
|
||||
info.format = image.format;
|
||||
info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers};
|
||||
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
|
||||
info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
viewInfos.push_back(info);
|
||||
passKey.attachments.push_back({image.storage, static_cast<Uint32>(surface.Kind), image.image,
|
||||
image.viewType, image.format, info.subresourceRange.aspectMask, aspect,
|
||||
image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height,
|
||||
image.samples});
|
||||
VkAttachmentDescription description{};
|
||||
description.format = image.format;
|
||||
description.samples = image.samples;
|
||||
description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
description.initialLayout = description.finalLayout = layout;
|
||||
attachments.push_back(description);
|
||||
extent.width = std::min(extent.width, image.extent.width);
|
||||
extent.height = std::min(extent.height, image.extent.height);
|
||||
layers = std::min(layers, image.layers);
|
||||
samples = image.samples;
|
||||
passHash = XXH64(&description, sizeof(description), passHash);
|
||||
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
||||
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
|
||||
if (image.isDefault) {
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
|
||||
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
|
||||
}
|
||||
return static_cast<Uint32>(attachments.size() - 1);
|
||||
};
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
||||
const Int32 slot = fbo->DrawBuffers[i];
|
||||
const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL});
|
||||
}
|
||||
while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back();
|
||||
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (attachments.empty()) {
|
||||
MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments");
|
||||
return false;
|
||||
}
|
||||
passKey.width = extent.width;
|
||||
passKey.height = extent.height;
|
||||
passKey.layers = layers;
|
||||
passKey.depthReference = depth.attachment;
|
||||
for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment);
|
||||
// A native handle can be recycled after image replacement or death.
|
||||
// Sample epochs after every lazy resolve, then compare them with the key.
|
||||
passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
|
||||
passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = static_cast<Uint32>(colors.size());
|
||||
subpass.pColorAttachments = colors.data();
|
||||
subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr;
|
||||
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
passInfo.attachmentCount = static_cast<Uint32>(attachments.size());
|
||||
passInfo.pAttachments = attachments.data();
|
||||
passInfo.subpassCount = 1;
|
||||
passInfo.pSubpasses = &subpass;
|
||||
if (!AcquireCachedWireDrawPass(passKey)) {
|
||||
m_wireDrawPassKey = Move(passKey);
|
||||
Vector<VkImageView> views;
|
||||
views.reserve(viewInfos.size());
|
||||
for (const auto& info : viewInfos) {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view));
|
||||
m_wireDrawViews.push_back(view);
|
||||
views.push_back(view);
|
||||
}
|
||||
m_wireDrawPass = MakeUnique<RenderPassEntry>();
|
||||
auto& created = *m_wireDrawPass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass));
|
||||
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
fbInfo.renderPass = created.renderPass;
|
||||
fbInfo.attachmentCount = static_cast<Uint32>(views.size());
|
||||
fbInfo.pAttachments = views.data();
|
||||
fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer));
|
||||
created.attachmentCount = static_cast<Uint32>(attachments.size());
|
||||
created.colorAttachmentCount = static_cast<Uint32>(colors.size());
|
||||
created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED;
|
||||
created.sampleCount = samples;
|
||||
created.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
|
||||
created.layers = layers;
|
||||
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
|
||||
}
|
||||
auto& pass = *m_wireDrawPass;
|
||||
WireRenderPassCompatibilityKey pipelinePassKey{};
|
||||
for (const auto& attachment : attachments)
|
||||
pipelinePassKey.attachmentFormatsAndSamples.push_back(
|
||||
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
|
||||
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
|
||||
pipelinePassKey.depthReference = depth.attachment;
|
||||
const Uint64 compatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
|
||||
const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass,
|
||||
ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw");
|
||||
return false;
|
||||
}
|
||||
for (Uint32 i = 0; i < vertexSlices.size(); ++i)
|
||||
vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset);
|
||||
Uint32 binding = static_cast<Uint32>(vertexSlices.size());
|
||||
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
|
||||
if (!(programObj.activeVertexInputLocationMask & (1u << location)) ||
|
||||
(input.attributeLocationMask & (1u << location))) continue;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
const void* data = nullptr;
|
||||
VkDeviceSize size = 0;
|
||||
if (!TryGetCurrentVertexAttributeUploadPayload(MGB_CTX->GetCurrentVertexAttribute(location),
|
||||
programObj.vertexInputTypes[location], format, data, size)) {
|
||||
MGL_WIRE_DECLINE_AT(CurrentAttributePayload, "a disabled array's current attribute value has no upload payload");
|
||||
return false;
|
||||
}
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
data, size, 16, slice)) {
|
||||
MGL_WIRE_DECLINE_AT(CurrentAttributeUpload, "the current attribute value did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset);
|
||||
}
|
||||
if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType);
|
||||
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) {
|
||||
MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun");
|
||||
return false;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault,
|
||||
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
|
||||
if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
|
||||
RetireWireDrawPass();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram);
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V");
|
||||
return;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
|
||||
return;
|
||||
}
|
||||
const auto pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (!pipeline) {
|
||||
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
|
||||
return;
|
||||
}
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound");
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdDispatch(frame.commandBuffer, x, y, z);
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
#version 450
|
||||
// SAMPLE ZERO, and it is not an approximation: VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is the mode the
|
||||
// VK_KHR_depth_stencil_resolve arm asks for, and GL 4.6 core 18.3.1 says a depth resolve picks
|
||||
// a value "in an implementation-dependent manner where the result will be between the minimum
|
||||
// and maximum depth values in the pixel" - sample 0 is one of the samples, so it qualifies, and
|
||||
// taking the same sample as the extension arm is what makes the two arms interchangeable on one
|
||||
// device.
|
||||
//
|
||||
// AVERAGING WOULD NOT BE: a mean of four depths is a surface that exists nowhere, and GL's
|
||||
// bound is on the per-pixel min/max, which a mean satisfies but a shadow comparison against it
|
||||
// does not survive.
|
||||
layout(set = 0, binding = 0) uniform sampler2DMS sourceImage;
|
||||
|
||||
void main() {
|
||||
gl_FragDepth = texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r;
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireDepthMipmap.inc.
|
||||
// The native multisample depth/stencil resolve owns no frontend object and never invokes the
|
||||
// GL shader compiler.
|
||||
//
|
||||
// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth
|
||||
// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment,
|
||||
// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this
|
||||
// package a device without either killed the session at
|
||||
// `Magma:multisample-depth-resolve-capability` the first time an application resolved a
|
||||
// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a
|
||||
// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both
|
||||
// arms; this is the second one.
|
||||
//
|
||||
// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the
|
||||
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
|
||||
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
|
||||
// requires a SELECTED sample for stencil anyway.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The pass's modules, layouts, render passes and pipeline, and the draw it records, are
|
||||
// WireDepthResolveProbe.cpp's (g5-msprobe): the multisample depth/stencil resolve probe runs
|
||||
// this exact arm as its CONTROL, so it is defined once for both.
|
||||
struct VulkanRenderer::WireMultisampleResolveResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = 0; // the ONE aspect this pipeline writes
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
WireShaderResolveKit kit;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireMultisampleResolveResources() {
|
||||
if (!m_wireMultisampleResolveResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit descriptor
|
||||
// pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireMultisampleResolveResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
DestroyWireShaderResolveKit(m_device, cache.kit);
|
||||
delete m_wireMultisampleResolveResources;
|
||||
m_wireMultisampleResolveResources = nullptr;
|
||||
}
|
||||
|
||||
// ONE ASPECT, source -> resolved, both 2D images of the same format and extent, the source
|
||||
// multisampled and the destination not. Returns false for every shape it cannot express,
|
||||
// which the caller turns into a decline; it never ends the session.
|
||||
Bool VulkanRenderer::ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
|
||||
VkImageAspectFlags aspect, GLint sx0, GLint sy0, Uint32 width, Uint32 height) {
|
||||
if (!source.image || !resolved.image || !width || !height) return false;
|
||||
if (aspect != VK_IMAGE_ASPECT_DEPTH_BIT && aspect != VK_IMAGE_ASPECT_STENCIL_BIT) return false;
|
||||
// A fragment shader cannot write the stencil aspect at all without
|
||||
// VK_EXT_shader_stencil_export. §3.2: decline stencil-only rather than produce a level
|
||||
// whose stencil is whatever the DONT_CARE left there.
|
||||
if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && !m_wireShaderStencilExport) return false;
|
||||
if (source.samples == VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
if (source.format != resolved.format) return false;
|
||||
// SAMPLED is asked for by every attachment VkTextureManager creates; a device that
|
||||
// declined it would make the descriptor write below undefined, not failed.
|
||||
if (!(source.usageFlags & VK_IMAGE_USAGE_SAMPLED_BIT)) return false;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
if ((properties.optimalTilingFeatures & required) != required) return false;
|
||||
|
||||
const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(source.format);
|
||||
if (!(formatAspects & aspect)) return false;
|
||||
|
||||
if (!m_wireMultisampleResolveResources) {
|
||||
m_wireMultisampleResolveResources = new WireMultisampleResolveResources;
|
||||
VK_VERIFY(CreateWireShaderResolveKit(m_device, m_wireShaderStencilExport,
|
||||
m_wireMultisampleResolveResources->kit));
|
||||
}
|
||||
auto& cache = *m_wireMultisampleResolveResources;
|
||||
const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.kit.stencilFragment
|
||||
: cache.kit.depthFragment;
|
||||
if (fragment == VK_NULL_HANDLE) return false;
|
||||
|
||||
WireMultisampleResolveResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets)
|
||||
if (entry.format == resolved.format && entry.aspect == aspect) { target = &entry; break; }
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = resolved.format;
|
||||
target->aspect = aspect;
|
||||
VK_VERIFY(CreateWireShaderResolveRenderPass(m_device, resolved.format, &target->renderPass));
|
||||
if (CreateWireShaderResolvePipeline(m_device, cache.kit, target->renderPass, aspect,
|
||||
&target->pipeline) != VK_SUCCESS) {
|
||||
vkDestroyRenderPass(m_device, target->renderPass, nullptr);
|
||||
cache.targets.pop_back();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
||||
|
||||
// ONE ASPECT on the sampled view: a depth/stencil image can only be SAMPLED through a
|
||||
// view that names exactly one of its aspects, which is also what makes the stencil half
|
||||
// a usampler2DMS and the depth half a sampler2DMS.
|
||||
const VkImageViewCreateInfo sourceViewInfo =
|
||||
WireDepthStencilViewInfo(source.image, source.format, aspect, source.level, source.layer);
|
||||
VkImageView sourceView = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &sourceViewInfo, nullptr, &sourceView));
|
||||
// BOTH aspects on the attachment view: a depth/stencil ATTACHMENT names every aspect
|
||||
// its format has, whichever one the pipeline writes.
|
||||
const VkImageViewCreateInfo targetViewInfo =
|
||||
WireDepthStencilViewInfo(resolved.image, resolved.format, formatAspects, 0, 0);
|
||||
VkImageView targetView = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &targetViewInfo, nullptr, &targetView));
|
||||
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &targetView;
|
||||
framebufferInfo.width = resolved.extent.width;
|
||||
framebufferInfo.height = resolved.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
|
||||
// Each in-flight pass owns its descriptor set, for the colour blit's reason: updating a
|
||||
// cached set would rewrite the source view of an older queued pass.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.kit.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
const VkDescriptorImageInfo sampled{cache.kit.nearest, sourceView,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
RecordWireShaderResolveDraw(frame.commandBuffer, cache.kit, target->renderPass, target->pipeline, framebuffer,
|
||||
descriptorSet, resolved.extent, {{sx0, sy0}, {width, height}});
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the application's
|
||||
// descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, targetView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,18 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan multisample depth/stencil resolve: no GL program, registry entry, or runtime
|
||||
// transpilation.
|
||||
//
|
||||
// THIS STAGE CARRIES NOTHING, and that is the whole design. The scratch this pass writes has
|
||||
// the SAME extent and the SAME coordinate space as the multisample source it reads - both are
|
||||
// native images, neither is flipped against the other - so the fragment stage can take its
|
||||
// coordinate straight out of gl_FragCoord and needs no varying, no push constant and no rect.
|
||||
// The requested rectangle is expressed by the scissor instead, which is also what the
|
||||
// VK_KHR_depth_stencil_resolve arm next door expresses with its renderArea.
|
||||
//
|
||||
// An oversized triangle rather than WireDepthMipmap.vert's quad: there is no destination
|
||||
// rectangle to clip to here, only the scissor, and a triangle rasterizes the same area with
|
||||
// one fewer primitive and no shared edge.
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(corners[gl_VertexIndex], 0.0, 1.0);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Generated from WireMultisampleResolve.vert / WireMultisampleDepthResolve.frag /
|
||||
// WireMultisampleStencilResolve.frag by scripts/bake_internal_shaders.py, which runs MG_Test's
|
||||
// BakedInternalShadersTest with the IN-TREE glslang and splices the words it emits into the
|
||||
// arrays below. Do not edit the words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test,
|
||||
// run as a gate) recompiles every source in its table and reds on the first word that
|
||||
// disagrees.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireMultisampleResolveVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000028u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000001au, 0x00030003u,
|
||||
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x0000000bu,
|
||||
0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u,
|
||||
0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
|
||||
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu,
|
||||
0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u,
|
||||
0x0000000du, 0x00000000u, 0x00060005u, 0x0000001au, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x0000001du, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030047u, 0x0000000bu, 0x00000002u,
|
||||
0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u,
|
||||
0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u,
|
||||
0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x0000001au, 0x0000000bu, 0x0000002au,
|
||||
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u,
|
||||
0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u,
|
||||
0x0004002bu, 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u,
|
||||
0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu,
|
||||
0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu,
|
||||
0x00000020u, 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u,
|
||||
0x00000006u, 0x00000002u, 0x0004002bu, 0x00000008u, 0x00000011u, 0x00000003u, 0x0004001cu, 0x00000012u,
|
||||
0x00000010u, 0x00000011u, 0x0004002bu, 0x00000006u, 0x00000013u, 0xbf800000u, 0x0005002cu, 0x00000010u,
|
||||
0x00000014u, 0x00000013u, 0x00000013u, 0x0004002bu, 0x00000006u, 0x00000015u, 0x40400000u, 0x0005002cu,
|
||||
0x00000010u, 0x00000016u, 0x00000015u, 0x00000013u, 0x0005002cu, 0x00000010u, 0x00000017u, 0x00000013u,
|
||||
0x00000015u, 0x0006002cu, 0x00000012u, 0x00000018u, 0x00000014u, 0x00000016u, 0x00000017u, 0x00040020u,
|
||||
0x00000019u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000019u, 0x0000001au, 0x00000001u, 0x00040020u,
|
||||
0x0000001cu, 0x00000007u, 0x00000012u, 0x00040020u, 0x0000001eu, 0x00000007u, 0x00000010u, 0x0004002bu,
|
||||
0x00000006u, 0x00000021u, 0x00000000u, 0x0004002bu, 0x00000006u, 0x00000022u, 0x3f800000u, 0x00040020u,
|
||||
0x00000026u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
|
||||
0x000200f8u, 0x00000005u, 0x0004003bu, 0x0000001cu, 0x0000001du, 0x00000007u, 0x0004003du, 0x0000000eu,
|
||||
0x0000001bu, 0x0000001au, 0x0003003eu, 0x0000001du, 0x00000018u, 0x00050041u, 0x0000001eu, 0x0000001fu,
|
||||
0x0000001du, 0x0000001bu, 0x0004003du, 0x00000010u, 0x00000020u, 0x0000001fu, 0x00050051u, 0x00000006u,
|
||||
0x00000023u, 0x00000020u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000024u, 0x00000020u, 0x00000001u,
|
||||
0x00070050u, 0x00000007u, 0x00000025u, 0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x00050041u,
|
||||
0x00000026u, 0x00000027u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000027u, 0x00000025u, 0x000100fdu,
|
||||
0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireMultisampleDepthResolveFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001du, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000008u, 0x00000010u, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u,
|
||||
0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x00000008u, 0x465f6c67u, 0x44676172u,
|
||||
0x68747065u, 0x00000000u, 0x00050005u, 0x0000000cu, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u,
|
||||
0x00000010u, 0x465f6c67u, 0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu,
|
||||
0x00000016u, 0x00040047u, 0x0000000cu, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000cu, 0x00000022u,
|
||||
0x00000000u, 0x00040047u, 0x00000010u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u,
|
||||
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040020u, 0x00000007u, 0x00000003u,
|
||||
0x00000006u, 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00090019u, 0x00000009u, 0x00000006u,
|
||||
0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x0000000au,
|
||||
0x00000009u, 0x00040020u, 0x0000000bu, 0x00000000u, 0x0000000au, 0x0004003bu, 0x0000000bu, 0x0000000cu,
|
||||
0x00000000u, 0x00040017u, 0x0000000eu, 0x00000006u, 0x00000004u, 0x00040020u, 0x0000000fu, 0x00000001u,
|
||||
0x0000000eu, 0x0004003bu, 0x0000000fu, 0x00000010u, 0x00000001u, 0x00040017u, 0x00000011u, 0x00000006u,
|
||||
0x00000002u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040017u, 0x00000015u, 0x00000014u,
|
||||
0x00000002u, 0x0004002bu, 0x00000014u, 0x00000017u, 0x00000000u, 0x00040015u, 0x0000001au, 0x00000020u,
|
||||
0x00000000u, 0x0004002bu, 0x0000001au, 0x0000001bu, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u,
|
||||
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000au, 0x0000000du, 0x0000000cu,
|
||||
0x0004003du, 0x0000000eu, 0x00000012u, 0x00000010u, 0x0007004fu, 0x00000011u, 0x00000013u, 0x00000012u,
|
||||
0x00000012u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000015u, 0x00000016u, 0x00000013u, 0x00040064u,
|
||||
0x00000009u, 0x00000018u, 0x0000000du, 0x0007005fu, 0x0000000eu, 0x00000019u, 0x00000018u, 0x00000016u,
|
||||
0x00000040u, 0x00000017u, 0x00050051u, 0x00000006u, 0x0000001cu, 0x00000019u, 0x00000000u, 0x0003003eu,
|
||||
0x00000008u, 0x0000001cu, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireMultisampleStencilResolveFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001fu, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
|
||||
0x00001395u, 0x0009000au, 0x5f565053u, 0x5f545845u, 0x64616873u, 0x735f7265u, 0x636e6574u, 0x655f6c69u,
|
||||
0x726f7078u, 0x00000074u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u,
|
||||
0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00000008u, 0x00000012u, 0x00030010u, 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x000013a3u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00090004u, 0x415f4c47u, 0x735f4252u, 0x65646168u, 0x74735f72u,
|
||||
0x69636e65u, 0x78655f6cu, 0x74726f70u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00080005u, 0x00000008u, 0x465f6c67u, 0x53676172u, 0x636e6574u, 0x65526c69u, 0x42524166u, 0x00000000u,
|
||||
0x00050005u, 0x0000000du, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u, 0x00000012u, 0x465f6c67u,
|
||||
0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu, 0x00001396u, 0x00040047u,
|
||||
0x0000000du, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000du, 0x00000022u, 0x00000000u, 0x00040047u,
|
||||
0x00000012u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
|
||||
0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040020u, 0x00000007u, 0x00000003u, 0x00000006u,
|
||||
0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00040015u, 0x00000009u, 0x00000020u, 0x00000000u,
|
||||
0x00090019u, 0x0000000au, 0x00000009u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u,
|
||||
0x00000000u, 0x0003001bu, 0x0000000bu, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000000u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000000u, 0x00030016u, 0x0000000fu, 0x00000020u, 0x00040017u,
|
||||
0x00000010u, 0x0000000fu, 0x00000004u, 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu,
|
||||
0x00000011u, 0x00000012u, 0x00000001u, 0x00040017u, 0x00000013u, 0x0000000fu, 0x00000002u, 0x00040017u,
|
||||
0x00000016u, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000006u, 0x00000018u, 0x00000000u, 0x00040017u,
|
||||
0x0000001au, 0x00000009u, 0x00000004u, 0x0004002bu, 0x00000009u, 0x0000001cu, 0x00000000u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000bu,
|
||||
0x0000000eu, 0x0000000du, 0x0004003du, 0x00000010u, 0x00000014u, 0x00000012u, 0x0007004fu, 0x00000013u,
|
||||
0x00000015u, 0x00000014u, 0x00000014u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000016u, 0x00000017u,
|
||||
0x00000015u, 0x00040064u, 0x0000000au, 0x00000019u, 0x0000000eu, 0x0007005fu, 0x0000001au, 0x0000001bu,
|
||||
0x00000019u, 0x00000017u, 0x00000040u, 0x00000018u, 0x00050051u, 0x00000009u, 0x0000001du, 0x0000001bu,
|
||||
0x00000000u, 0x0004007cu, 0x00000006u, 0x0000001eu, 0x0000001du, 0x0003003eu, 0x00000008u, 0x0000001eu,
|
||||
0x000100fdu, 0x00010038u,
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
#version 450
|
||||
// The stencil half, and it needs an extension to exist at all: a fragment shader cannot write
|
||||
// the stencil aspect without VK_EXT_shader_stencil_export (SPV_EXT_shader_stencil_export, which
|
||||
// this `#extension` line is how glslang is asked for). CONTRACT-P7 §3.2 says so and says what
|
||||
// happens otherwise - a stencil-only resolve DECLINES on a device without it, because there is
|
||||
// no second way to put a value in the stencil aspect from a pass.
|
||||
//
|
||||
// usampler2DMS and not sampler2DMS: the stencil aspect of a depth/stencil image is an unsigned
|
||||
// integer, and a float view of it would round the index.
|
||||
//
|
||||
// Sample zero, for WireMultisampleDepthResolve.frag's reason, and GL is stricter here: 18.3.1
|
||||
// says integer and stencil resolves SELECT one sample rather than combining them, so sample 0
|
||||
// is the answer and not merely an acceptable one.
|
||||
#extension GL_ARB_shader_stencil_export : require
|
||||
layout(set = 0, binding = 0) uniform usampler2DMS sourceImage;
|
||||
|
||||
void main() {
|
||||
gl_FragStencilRefARB = int(texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r);
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// UniformManager-owned native descriptors for wire bindings with no image.
|
||||
// No GL object is constructed or inserted into the client's resource namespace.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void UniformManager::DestroyWirePlaceholderImage(WirePlaceholderImage& image) const {
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
if (image.clearFramebuffer) vkDestroyFramebuffer(m_device, image.clearFramebuffer, nullptr);
|
||||
if (image.clearPass) vkDestroyRenderPass(m_device, image.clearPass, nullptr);
|
||||
if (image.sampler) vkDestroySampler(m_device, image.sampler, nullptr);
|
||||
if (image.view) vkDestroyImageView(m_device, image.view, nullptr);
|
||||
if (image.image) vkDestroyImage(m_device, image.image, nullptr);
|
||||
if (image.memory) vkFreeMemory(m_device, image.memory, nullptr);
|
||||
}
|
||||
image = {};
|
||||
}
|
||||
|
||||
void UniformManager::SetWireInvalidStorageImageArm(Bool deviceNullDescriptor) {
|
||||
const char* forced = std::getenv("MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER");
|
||||
const Bool forcePrivate = forced != nullptr && std::strcmp(forced, "1") == 0;
|
||||
m_wireInvalidStorageImagesBindNull = deviceNullDescriptor && !forcePrivate;
|
||||
MGLOG_I("Magma wire: an invalid or unbound image unit binds %s (VK_EXT_robustness2 nullDescriptor %s%s)",
|
||||
m_wireInvalidStorageImagesBindNull ? "a NULL storage descriptor"
|
||||
: "a placeholder private to its unit",
|
||||
deviceNullDescriptor ? "enabled" : "unavailable",
|
||||
deviceNullDescriptor && forcePrivate ? "; MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER=1" : "");
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveWirePlaceholderImage(VkCommandBuffer commands, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
|
||||
VkDescriptorImageInfo& out, VkFormat boundStorageFormat, Uint32 unit) const {
|
||||
// GL 4.6 core 8.26, codex closeout finding 2: every access through an invalid image unit
|
||||
// is invalid - a load returns zero, a store and an atomic are discarded. A NULL storage
|
||||
// descriptor (VK_EXT_robustness2 nullDescriptor) is exactly that, for any target, format
|
||||
// and sample count, so nothing below is needed.
|
||||
if (storage && m_wireInvalidStorageImagesBindNull) {
|
||||
out = {VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL};
|
||||
return true;
|
||||
}
|
||||
if (binding >= programObj.samplerTextureTargetByBinding.size() ||
|
||||
binding >= programObj.samplerNumericDomainByBinding.size()) return false;
|
||||
const auto target = programObj.samplerTextureTargetByBinding[binding];
|
||||
const auto domain = programObj.samplerNumericDomainByBinding[binding];
|
||||
const Bool multisample = target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray;
|
||||
Bool shadow = false;
|
||||
if (!storage) {
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
switch (program.GetUniformType(static_cast<Uint>(location))) {
|
||||
case GL_SAMPLER_1D_SHADOW: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_CUBE_SHADOW:
|
||||
case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW:
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW: case GL_SAMPLER_2D_RECT_SHADOW: shadow = true; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
if (storage) {
|
||||
if (binding >= programObj.storageImageFormatByBinding.size()) return false;
|
||||
format = programObj.storageImageFormatByBinding[binding];
|
||||
// A formatless declaration of an INVALID binding (a unit whose texture lacks the
|
||||
// named level or layer) takes the format the application bound, where the device can
|
||||
// store to it and it is in the shader's numeric class. The R32 default expands a load
|
||||
// to (0, 0, 0, 1); a four-channel carrier cleared to zero reads (0, 0, 0, 0), which
|
||||
// is what KHR-GL46.shader_image_load_store.incomplete_textures compares against.
|
||||
if (format == VK_FORMAT_UNDEFINED && boundStorageFormat != VK_FORMAT_UNDEFINED) {
|
||||
const Bool classMatches = domain == SamplerNumericDomain::Float
|
||||
? !vkuFormatIsUINT(boundStorageFormat) && !vkuFormatIsSINT(boundStorageFormat)
|
||||
: domain == SamplerNumericDomain::SignedInteger ? vkuFormatIsSINT(boundStorageFormat)
|
||||
: domain == SamplerNumericDomain::UnsignedInteger && vkuFormatIsUINT(boundStorageFormat);
|
||||
VkFormatProperties properties{};
|
||||
if (classMatches) vkGetPhysicalDeviceFormatProperties(m_physicalDevice, boundStorageFormat, &properties);
|
||||
if (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) format = boundStorageFormat;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED) format = PlaceholderFormatForNumericDomain(domain);
|
||||
} else if (shadow) format = VK_FORMAT_D16_UNORM;
|
||||
else if (domain == SamplerNumericDomain::Float) format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
else if (domain == SamplerNumericDomain::SignedInteger) format = VK_FORMAT_R8G8B8A8_SINT;
|
||||
else if (domain == SamplerNumericDomain::UnsignedInteger) format = VK_FORMAT_R8G8B8A8_UINT;
|
||||
if (format == VK_FORMAT_UNDEFINED) return false;
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageCreateFlags flags = 0;
|
||||
Uint32 layers = 1;
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D; break;
|
||||
case TextureTarget::Texture1DArray: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY; break;
|
||||
case TextureTarget::Texture2D: case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample: break;
|
||||
case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisampleArray:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; break;
|
||||
case TextureTarget::Texture3D: imageType = VK_IMAGE_TYPE_3D; viewType = VK_IMAGE_VIEW_TYPE_3D; break;
|
||||
case TextureTarget::TextureCubeMap: case TextureTarget::TextureCubeMapArray:
|
||||
flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; layers = 6;
|
||||
viewType = target == TextureTarget::TextureCubeMap ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
break;
|
||||
default: return false;
|
||||
}
|
||||
// A STORAGE placeholder is private to its unit (per shape). Shared by shape alone, a store
|
||||
// through invalid unit A was visible, after a barrier, to a load through invalid unit B of
|
||||
// the same shape - one Vulkan image behind both (codex closeout finding 2). The clear
|
||||
// below still runs before each use; what a private image cannot hide is the SAME unit's
|
||||
// own store to its own later load inside one pass (CONTRACT-P7 §12). Not per binding: GL
|
||||
// aliases per unit, and a (binding, unit) key only multiplied allocations. Past
|
||||
// kWirePrivateStoragePlaceholderCap a new (shape, unit) shares its shape's (WirePlaceholderKey.h).
|
||||
const Uint64 shape = static_cast<Uint32>(format) | (static_cast<Uint64>(target) << 32) |
|
||||
(static_cast<Uint64>(storage) << 48) | (static_cast<Uint64>(shadow) << 49);
|
||||
const WirePlaceholderKey key = ChooseWirePlaceholderKey(shape, storage, unit, m_wirePrivateStoragePlaceholders,
|
||||
[this](const WirePlaceholderKey& candidate) { return m_wirePlaceholderImages.count(candidate) != 0; });
|
||||
if (storage && unit != WirePlaceholderKey::kShared && key.unit == WirePlaceholderKey::kShared) {
|
||||
MGLOG_W_ONCE("Magma wire: %zu image-unit-private placeholders exist (the cap); a further invalid or "
|
||||
"unbound unit shares its shape's placeholder, so two such units of one shape can alias",
|
||||
static_cast<size_t>(m_wirePrivateStoragePlaceholders));
|
||||
}
|
||||
auto found = m_wirePlaceholderImages.find(key);
|
||||
if (found == m_wirePlaceholderImages.end()) {
|
||||
WirePlaceholderImage image{};
|
||||
image.format = format; image.layers = layers;
|
||||
image.aspect = shadow ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
const auto fail = [&] {
|
||||
MGLOG_E_ONCE("Magma wire placeholder allocation failed: target=%d format=%d storage=%d multisample=%d",
|
||||
static_cast<Int>(target), static_cast<Int>(format), static_cast<Int>(storage), static_cast<Int>(multisample));
|
||||
DestroyWirePlaceholderImage(image);
|
||||
return false;
|
||||
};
|
||||
const VkImageUsageFlags usage = (storage ? VK_IMAGE_USAGE_STORAGE_BIT : VK_IMAGE_USAGE_SAMPLED_BIT) |
|
||||
(multisample ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : VK_IMAGE_USAGE_TRANSFER_DST_BIT);
|
||||
VkImageFormatProperties properties{};
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, imageType,
|
||||
VK_IMAGE_TILING_OPTIMAL, usage, flags, &properties) != VK_SUCCESS) return fail();
|
||||
if (multisample) {
|
||||
// MS=1 in SPIR-V cannot name a single-sampled image, even when
|
||||
// the shader's runtime branch does not fetch that descriptor.
|
||||
image.samples = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM;
|
||||
for (const auto sample : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT,
|
||||
VK_SAMPLE_COUNT_16_BIT, VK_SAMPLE_COUNT_32_BIT, VK_SAMPLE_COUNT_64_BIT}) {
|
||||
if (properties.sampleCounts & sample) { image.samples = sample; break; }
|
||||
}
|
||||
if (image.samples == VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM) return fail();
|
||||
}
|
||||
VkImageCreateInfo create{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
create.flags = flags; create.imageType = imageType; create.format = format;
|
||||
create.extent = {1, 1, 1}; create.mipLevels = 1; create.arrayLayers = layers;
|
||||
create.samples = image.samples; create.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
create.usage = usage; create.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateImage(m_device, &create, nullptr, &image.image) != VK_SUCCESS) return fail();
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(m_device, image.image, &requirements);
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memoryProperties);
|
||||
Uint32 memoryType = ~0u;
|
||||
for (Uint32 i = 0; i < memoryProperties.memoryTypeCount; ++i) {
|
||||
if (!(requirements.memoryTypeBits & (1u << i))) continue;
|
||||
if (memoryType == ~0u) memoryType = i;
|
||||
if (memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
|
||||
memoryType = i; break;
|
||||
}
|
||||
}
|
||||
if (memoryType == ~0u) return fail();
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = memoryType;
|
||||
if (vkAllocateMemory(m_device, &allocate, nullptr, &image.memory) != VK_SUCCESS ||
|
||||
vkBindImageMemory(m_device, image.image, image.memory, 0) != VK_SUCCESS) return fail();
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image; view.viewType = viewType; view.format = format;
|
||||
view.subresourceRange = {image.aspect, 0, 1, 0, layers};
|
||||
if (vkCreateImageView(m_device, &view, nullptr, &image.view) != VK_SUCCESS) return fail();
|
||||
if (!storage) {
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.compareEnable = shadow; sampler.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||
sampler.maxLod = 0.0f;
|
||||
if (vkCreateSampler(m_device, &sampler, nullptr, &image.sampler) != VK_SUCCESS) return fail();
|
||||
}
|
||||
if (multisample) {
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format; attachment.samples = image.samples;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1; pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1; pass.pSubpasses = &subpass;
|
||||
VkSubpassDependency dependencies[2]{};
|
||||
dependencies[0] = {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0};
|
||||
dependencies[1] = {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0};
|
||||
pass.dependencyCount = 2; pass.pDependencies = dependencies;
|
||||
if (vkCreateRenderPass(m_device, &pass, nullptr, &image.clearPass) != VK_SUCCESS) return fail();
|
||||
VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebuffer.renderPass = image.clearPass; framebuffer.attachmentCount = 1;
|
||||
framebuffer.pAttachments = &image.view; framebuffer.width = framebuffer.height = 1; framebuffer.layers = layers;
|
||||
if (vkCreateFramebuffer(m_device, &framebuffer, nullptr, &image.clearFramebuffer) != VK_SUCCESS) return fail();
|
||||
}
|
||||
found = m_wirePlaceholderImages.emplace(key, image).first;
|
||||
if (key.unit != WirePlaceholderKey::kShared) ++m_wirePrivateStoragePlaceholders;
|
||||
}
|
||||
auto& image = found->second;
|
||||
// Sampled incomplete textures remain immutable opaque black. A storage
|
||||
// placeholder is reset before each use, so a prior discarded store cannot
|
||||
// leak into a later pass's loads through the same unit.
|
||||
if (storage || image.layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
VkClearValue clear{};
|
||||
if (!storage) {
|
||||
if (domain == SamplerNumericDomain::SignedInteger) clear.color.int32[3] = 1;
|
||||
else if (domain == SamplerNumericDomain::UnsignedInteger) clear.color.uint32[3] = 1;
|
||||
else clear.color.float32[3] = 1.0f;
|
||||
}
|
||||
if (multisample) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = image.clearPass; begin.framebuffer = image.clearFramebuffer;
|
||||
begin.renderArea.extent = {1, 1}; begin.clearValueCount = 1; begin.pClearValues = &clear;
|
||||
vkCmdBeginRenderPass(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(commands);
|
||||
image.layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
} else {
|
||||
if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, image.aspect)) return false;
|
||||
const VkImageSubresourceRange range{image.aspect, 0, 1, 0, image.layers};
|
||||
if (shadow) {
|
||||
const VkClearDepthStencilValue depth{0.0f, 0};
|
||||
vkCmdClearDepthStencilImage(commands, image.image, image.layout, &depth, 1, &range);
|
||||
} else vkCmdClearColorImage(commands, image.image, image.layout, &clear.color, 1, &range);
|
||||
if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
image.aspect)) return false;
|
||||
}
|
||||
VkMemoryBarrier visible{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
visible.srcAccessMask = multisample ? VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
visible.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, multisample ? VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &visible, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
out = {image.sampler, image.view, VK_IMAGE_LAYOUT_GENERAL};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,62 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderKey.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
// The pure half of UniformManager's wire placeholder cache (WirePlaceholderImages.inc): WHICH
|
||||
// placeholder a binding with no usable image takes. A unit test pins it
|
||||
// (MG_Test/Pipeline/PipelineQuirkTest.cpp); the Vulkan half that makes the image stays in the .inc.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The placeholder's SHAPE (format, target, storage, shadow) and, for a STORAGE placeholder on
|
||||
// the arm without a null descriptor, the image UNIT it is private to. GL aliases per UNIT, so the
|
||||
// unit is the whole of the no-alias guarantee (codex closeout finding 2): two bindings that name
|
||||
// one unit are one GL image, and keying them apart as well only multiplied allocations (cf-magma
|
||||
// critic). A sampled placeholder is never written, so it stays shared: its unit is kShared.
|
||||
struct WirePlaceholderKey {
|
||||
static constexpr Uint32 kShared = ~0u;
|
||||
Uint64 shape = 0;
|
||||
Uint32 unit = kShared;
|
||||
Bool operator==(const WirePlaceholderKey& other) const { return shape == other.shape && unit == other.unit; }
|
||||
};
|
||||
|
||||
struct WirePlaceholderKeyHash {
|
||||
SizeT operator()(const WirePlaceholderKey& key) const {
|
||||
Uint64 h = key.shape * 0x9E3779B97F4A7C15ull;
|
||||
h ^= static_cast<Uint64>(key.unit) + 0x9E3779B97F4A7C15ull + (h << 6) + (h >> 2);
|
||||
return static_cast<SizeT>(h);
|
||||
}
|
||||
};
|
||||
|
||||
// Every private placeholder is its own vkCreateImage + vkAllocateMemory, freed only at Shutdown.
|
||||
// (shape, unit) bounds them by shapes x kMGPipeMaxImageUnits, and a peer rotating formats and
|
||||
// targets across the units could still walk that towards maxMemoryAllocationCount (4096 is the
|
||||
// spec minimum). So past this many, a NEW (shape, unit) takes its shape's SHARED placeholder - the
|
||||
// pre-fix aliasing, logged once by the caller - instead of another allocation; a unit that already
|
||||
// has its private placeholder keeps it. Two per unit is far past any real program.
|
||||
inline constexpr SizeT kWirePrivateStoragePlaceholderCap = 2 * static_cast<SizeT>(MG_Pipe::kMGPipeMaxImageUnits);
|
||||
|
||||
// The key a placeholder of `shape` is found or made under. `privateStorage` = a storage binding on
|
||||
// the arm without a null descriptor (with one, the caller never gets here); `unit` = its image
|
||||
// unit; `privateCount` = how many private placeholders exist; `known(key)` = the cache holds `key`.
|
||||
template <typename KnownFn>
|
||||
WirePlaceholderKey ChooseWirePlaceholderKey(Uint64 shape, Bool privateStorage, Uint32 unit, SizeT privateCount,
|
||||
KnownFn&& known) {
|
||||
WirePlaceholderKey key;
|
||||
key.shape = shape;
|
||||
if (!privateStorage || unit == WirePlaceholderKey::kShared) return key;
|
||||
key.unit = unit;
|
||||
if (privateCount < kWirePrivateStoragePlaceholderCap || known(key)) return key;
|
||||
key.unit = WirePlaceholderKey::kShared;
|
||||
return key;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,49 +0,0 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The wire draw pass has one graphics subpass, no input attachments,
|
||||
// multiview or extensions. Its Vulkan pipeline compatibility is formats,
|
||||
// sample counts and the ordered attachment references, not native handles,
|
||||
// image identity, dimensions, load/store operations or image layouts.
|
||||
struct WireRenderPassCompatibilityKey {
|
||||
Vector<Uint64> attachmentFormatsAndSamples;
|
||||
Vector<Uint32> colorReferences;
|
||||
Uint32 depthReference = VK_ATTACHMENT_UNUSED;
|
||||
Bool operator==(const WireRenderPassCompatibilityKey&) const = default;
|
||||
};
|
||||
|
||||
struct WireRenderPassCompatibilityHash {
|
||||
SizeT operator()(const WireRenderPassCompatibilityKey& key) const {
|
||||
SizeT hash = 0;
|
||||
const auto mix = [&](Uint64 value) {
|
||||
hash ^= static_cast<SizeT>(value) + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2);
|
||||
};
|
||||
mix(key.attachmentFormatsAndSamples.size());
|
||||
for (const auto value : key.attachmentFormatsAndSamples) mix(value);
|
||||
mix(key.colorReferences.size());
|
||||
for (const auto value : key.colorReferences) mix(value);
|
||||
mix(key.depthReference);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
class WireRenderPassCompatibilityTable {
|
||||
public:
|
||||
Uint64 Intern(const WireRenderPassCompatibilityKey& key) {
|
||||
if (const auto found = m_ids.find(key); found != m_ids.end()) return found->second;
|
||||
// No reset/eviction: an ID cannot be reused while a pipeline factory
|
||||
// or a renderer memo can still hold its previous meaning.
|
||||
MOBILEGL_ASSERT(m_nextId != 0, "wire render-pass compatibility ID exhausted");
|
||||
const Uint64 id = m_nextId++;
|
||||
m_ids.emplace(key, id);
|
||||
return id;
|
||||
}
|
||||
private:
|
||||
UnorderedMap<WireRenderPassCompatibilityKey, Uint64, WireRenderPassCompatibilityHash> m_ids;
|
||||
Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -1,214 +0,0 @@
|
||||
// Server-owned texture readback. Included after WireFramebuffer.inc so image
|
||||
// layout tracking and frame retirement are the same as every other wire verb.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
|
||||
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
|
||||
Vector<Uint8>& result) {
|
||||
using namespace DirectGLES::ReadbackImpl;
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(info.Format, mapping)) return false;
|
||||
const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format);
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type);
|
||||
const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D;
|
||||
if (!info.Box.W || !rows64 || rows64 > std::numeric_limits<GLsizei>::max() ||
|
||||
rows64 > std::numeric_limits<SizeT>::max() / 16 / info.Box.W) return false;
|
||||
const SizeT pixels = static_cast<SizeT>(info.Box.W) * static_cast<SizeT>(rows64);
|
||||
if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize ||
|
||||
info.DstSize != pixels * pixelBytes) return false;
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
||||
if (pixelBytes != sourcePixelSize) return false;
|
||||
result.assign(source, source + sourceBytes);
|
||||
return true;
|
||||
}
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
const GLsizei rows = static_cast<GLsizei>(Uint64(info.Box.H) * info.Box.D);
|
||||
if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) ||
|
||||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
|
||||
if (ResolveTextureFormatInfo(logicalFormat).expandRgbToRgba) {
|
||||
const Float floatOne = 1.0f;
|
||||
const Uint32 integerOne = 1;
|
||||
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
||||
for (SizeT i = 0; i < pixels; ++i)
|
||||
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
||||
}
|
||||
result.resize(info.DstSize);
|
||||
for (GLsizei row = 0; row < rows; ++row)
|
||||
ConvertWideReadbackRow(wide.data() + row * static_cast<SizeT>(info.Box.W) * 16,
|
||||
result.data() + row * static_cast<SizeT>(info.Box.W) * pixelBytes,
|
||||
info.Box.W, wideType, mapping, info.Type);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info,
|
||||
Vector<Uint8>& ownedBytes) {
|
||||
ownedBytes.clear();
|
||||
const auto& box = info.Box;
|
||||
if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D ||
|
||||
box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
||||
Uint64(box.H) * box.D > static_cast<Uint64>(std::numeric_limits<GLsizei>::max())) return false;
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false;
|
||||
const auto& record = state.TextureResources[info.Res.Slot];
|
||||
if (!record.Live || record.Gen != info.Res.Gen) return false;
|
||||
|
||||
using Target = MG_Pipe::MGPipeResourceTarget;
|
||||
const auto target = static_cast<Target>(record.Desc.Target);
|
||||
const Bool oneDArray = target == Target::Tex1DArray;
|
||||
const Bool volume = target == Target::Tex3D;
|
||||
Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1;
|
||||
VkExtent3D extent{box.W, box.H, 1};
|
||||
if (oneDArray) {
|
||||
if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false;
|
||||
layerCount = box.H; extent.height = 1;
|
||||
} else if (volume) {
|
||||
if (info.Target != GL_TEXTURE_3D) return false;
|
||||
extent.depth = box.D;
|
||||
} else if (target == Target::Tex2DArray || target == Target::TexCubeArray) {
|
||||
if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY)) return false;
|
||||
layerCount = box.D;
|
||||
} else if (target == Target::TexCube) {
|
||||
if (info.Target == GL_TEXTURE_CUBE_MAP) {
|
||||
if (box.D != 6) return false;
|
||||
layerCount = 6;
|
||||
} else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
|
||||
if (box.D != 1) return false;
|
||||
layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X;
|
||||
} else return false;
|
||||
} else {
|
||||
if (box.D != 1) return false;
|
||||
const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D :
|
||||
target == Target::Tex2D ? GL_TEXTURE_2D :
|
||||
target == Target::TexRect ? GL_TEXTURE_RECTANGLE : GL_NONE;
|
||||
if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false;
|
||||
}
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(storage)) return false;
|
||||
const Uint64 pixels64 = Uint64(box.W) * box.H * box.D;
|
||||
if (pixels64 > std::numeric_limits<SizeT>::max() / 16) return false;
|
||||
const auto readUnbacked = [&]() {
|
||||
// Only a level the VkImage cannot represent may use its owned,
|
||||
// non-GPU-dirty staging bytes. A failed GPU read never falls back.
|
||||
if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false;
|
||||
Vector<Uint8> raw;
|
||||
VkFormat nativeFormat = VK_FORMAT_UNDEFINED;
|
||||
if (!m_textureManager->ReadUnbackedWireLevel(storage,
|
||||
MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
|
||||
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
|
||||
nativeFormat, raw)) return false;
|
||||
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
|
||||
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
|
||||
};
|
||||
RetireWireDrawPass();
|
||||
MakeXfbWritesVisible();
|
||||
auto* resource = m_textureManager->SyncTextureResourceByHandle(storage);
|
||||
if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked();
|
||||
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
||||
const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers;
|
||||
if (extent.width != std::max(resource->extent.width >> level, 1u) ||
|
||||
extent.height != std::max(resource->extent.height >> level, 1u) ||
|
||||
layer >= nativeDepth || (nativeVolume ? extent.depth : layerCount) > nativeDepth - layer ||
|
||||
(!nativeVolume && layerCount > availableLayers)) return readUnbacked();
|
||||
// A view can reinterpret the same native words. Decode using the outer
|
||||
// view format, while Vulkan copies the storage allocation's bytes.
|
||||
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
|
||||
const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL;
|
||||
const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL;
|
||||
const Bool depthStencil = depth || stencil;
|
||||
const VkImageAspectFlags aspects = depthStencil ?
|
||||
((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)) : VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
if ((resource->aspect & aspects) != aspects) return false;
|
||||
const SizeT pixels = static_cast<SizeT>(pixels64);
|
||||
const SizeT destinationPixelSize = MG_Util::GetInputBytesPerPixel(
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
|
||||
if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false;
|
||||
SizeT sourcePixelSize = GetReadbackTexelSize(resource->format);
|
||||
if (depthStencil) {
|
||||
switch (resource->format) {
|
||||
case VK_FORMAT_D16_UNORM: sourcePixelSize = 2; break;
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_X8_D24_UNORM_PACK32:
|
||||
case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: sourcePixelSize = 4; break;
|
||||
case VK_FORMAT_S8_UINT: sourcePixelSize = 0; break;
|
||||
default: return false;
|
||||
}
|
||||
} else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format)) return false;
|
||||
const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize);
|
||||
const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3};
|
||||
const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes;
|
||||
if (!copyBytes) return false;
|
||||
VkBufferObject readback;
|
||||
if (!readback.Create({.allocator = m_allocator, .size = copyBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) return false;
|
||||
WireImage image{};
|
||||
image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect;
|
||||
image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels;
|
||||
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
|
||||
TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
VkBufferImageCopy regions[2]{};
|
||||
Uint32 regionCount = 0;
|
||||
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
|
||||
auto& copy = regions[regionCount++];
|
||||
copy.bufferOffset = offset;
|
||||
copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount};
|
||||
copy.imageOffset = {0, 0, nativeVolume ? static_cast<Int32>(layer) : 0};
|
||||
copy.imageExtent = extent;
|
||||
};
|
||||
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
||||
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
||||
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
readback.GetHandle(), regionCount, regions);
|
||||
TransitionWireImage(image, restore);
|
||||
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &host, 0, nullptr, 0, nullptr);
|
||||
if (!SubmitReadbackCommandsAndWait(frame)) return false;
|
||||
const auto* mapped = static_cast<const Uint8*>(readback.Map());
|
||||
if (!mapped || !readback.Invalidate(copyBytes)) return false;
|
||||
Vector<Uint8> result;
|
||||
if (depthStencil) {
|
||||
if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr,
|
||||
resource->format, pixels, info.Format, info.Type, result)) return false;
|
||||
} else {
|
||||
using namespace DirectGLES::ReadbackImpl;
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(info.Format, mapping) ||
|
||||
GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false;
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
||||
if (destinationPixelSize != sourcePixelSize) return false;
|
||||
result.assign(mapped, mapped + firstBytes);
|
||||
} else {
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
const GLsizei rows = static_cast<GLsizei>(Uint64(box.H) * box.D);
|
||||
if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) ||
|
||||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
|
||||
if (ResolveTextureFormatInfo(static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba) {
|
||||
const Float floatOne = 1.0f;
|
||||
const Uint32 integerOne = 1;
|
||||
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
||||
for (SizeT i = 0; i < pixels; ++i)
|
||||
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
||||
}
|
||||
result.resize(pixels * destinationPixelSize);
|
||||
const SizeT srcRow = static_cast<SizeT>(box.W) * 4 * sizeof(Uint32);
|
||||
const SizeT dstRow = static_cast<SizeT>(box.W) * destinationPixelSize;
|
||||
for (GLsizei row = 0; row < rows; ++row)
|
||||
ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow,
|
||||
box.W, wideType, mapping, info.Type);
|
||||
}
|
||||
}
|
||||
if (result.size() != info.DstSize) return false;
|
||||
ownedBytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,63 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Config.h>
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
|
||||
// shared by capability advertisement (BackendObject) and module lowering
|
||||
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
|
||||
//
|
||||
// Native subgroups are the implementation whenever the device has them, whatever
|
||||
// their width - subgroup operations execute on the hardware paths they were made
|
||||
// for. Module-level repairs keep the GL contract intact around them:
|
||||
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
||||
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
||||
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
|
||||
// reusing that scratch, when explicitly enabled;
|
||||
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
||||
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
||||
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
||||
// devices with no subgroup support at all, and only when the user opts in with
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
|
||||
|
||||
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
|
||||
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
|
||||
inline constexpr Uint32 kEmulatedSubgroupFeatures =
|
||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
|
||||
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
||||
|
||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||
!MG_Config::Features.MagmaDisableSubgroup;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||
// grows one under-declared array; every other module passes through untouched.
|
||||
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
|
||||
MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
inline Bool ShouldFixIterationRPBarrier() {
|
||||
return MG_Config::Features.MagmaIterationRPFixBarrier;
|
||||
}
|
||||
|
||||
inline Bool ShouldDeriveNumSubgroups() {
|
||||
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
|
||||
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
||||
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
||||
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -52,60 +52,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
|
||||
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
|
||||
// attachment view (and render pass format) therefore drops to the UNORM twin
|
||||
// whenever the capability is off. Sampled views keep the sRGB format (decode on
|
||||
// sample is unconditional in GL).
|
||||
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
|
||||
if (framebufferSrgbEnabled) return format;
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
default:
|
||||
return format;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||
// call: appending its format to the base format while its arguments precede the base
|
||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||
//
|
||||
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
|
||||
// MobileGL believes it has already made legal came back non-success, which is a
|
||||
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
|
||||
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
|
||||
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
|
||||
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
|
||||
// the assert is compiled out by contract.
|
||||
//
|
||||
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
|
||||
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
|
||||
// where a driver legitimately refuses an image the format pre-check accepted.
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
if (_vk_verify_result != VK_SUCCESS) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define XXHASH_VERIFY(expr, ...) \
|
||||
do { \
|
||||
XXH_errorcode _xxh_verify_result = (expr); \
|
||||
if (_xxh_verify_result != XXH_OK) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
|
||||
__LINE__); \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
@@ -11,28 +11,6 @@
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/FatalFunnel.h> // InstallPipeSessionFailHook, installed at step 0 below
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// [p5/v1-joint PREVIEW EDIT - the merge-time form of v1's hook, c1-v1 8.2's exact order.] The
|
||||
// weak CreateRemoteBackendObject placeholder v1 shipped for a c1-less tree is DELETED at the
|
||||
// merge: the integration test links the STATIC MobileGL_s archive, and an archive member is
|
||||
// only pulled to satisfy an UNDEFINED reference - the weak definition in ServerLoop.cpp.o
|
||||
// already satisfied it, so BackendObject_Remote.cpp.o was never linked at all and the joint
|
||||
// build aborted in the placeholder (~/w7/p5-v1-joint-preflight.log). Direct construction
|
||||
// needs no factory and no weak symbol.
|
||||
#include <MG_Remote/Client/BackendObject_Remote.h>
|
||||
#endif
|
||||
|
||||
#include "ServerRole.h"
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
void LogBackendInfo() {
|
||||
if (!pActiveBackendObject) {
|
||||
@@ -67,372 +45,9 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace {
|
||||
// WHICH MGPipe SUBSYSTEMS THIS SERVER HAS A CONSUMER FOR - CallMask bits 32..47 (R-8 /
|
||||
// C-4). It is stated from what the server's own backend IS, and NOT derived from
|
||||
// MGPipeGetResourceOps(): that is a PROCESS-WIDE global, so under inproc a derivation
|
||||
// would answer with whatever the client half of the same process registered and under
|
||||
// spawn it would collapse to P2's 0x7f. Either way CapsMirror::ServerConsumes would
|
||||
// then answer a client-side liveness gate with a guess, the client would stop emitting
|
||||
// whole record families, clear its dirty flags on acceptance anyway, and the lane would
|
||||
// go green with the uploads lost - ID-39's 66 lost uploads, reflected (ServerSession.h).
|
||||
//
|
||||
// DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's
|
||||
// 9..12): it registers the resource op table in Initialize()
|
||||
// (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs.
|
||||
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
|
||||
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
|
||||
// the same fact ObjectSubsystemControlScenario already pins from the client side.
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
|
||||
//
|
||||
// kCapRunAheadApply says "this server applies an unbarriered record without reading
|
||||
// client memory". That is only true once EVERY per-draw family reads records instead
|
||||
// of the frontend - vi, sb, pg, tx2 and fb all land before it is - so the caps arm
|
||||
// below is gated on this constant, which the P5e INTEGRATION COMMIT flips to true
|
||||
// after the last of them. Until then every P5e package lands with the wait rule, the
|
||||
// static WaitClass column and the barriered predicate compiled and INERT: the client
|
||||
// never latches RunAheadArmed, so it runs today's lockstep path byte for byte.
|
||||
//
|
||||
// It is a constant and not a knob on purpose. An operator cannot turn a half-migrated
|
||||
// server into a run-ahead one, because the failure mode is not a slow frame - it is
|
||||
// the apply thread reading client memory that has already moved, which renders wrong
|
||||
// rather than aborting.
|
||||
constexpr Bool kMGPipeP5eRunAheadReady = true;
|
||||
constexpr Bool kMGPipeMagmaRunAheadReady = true;
|
||||
|
||||
// The Magma transport now owns buffer stores and consumes their vertex,
|
||||
// index and shader binding records. Both backends publish the resource
|
||||
// family only alongside a real op table (checked below). This does not
|
||||
// itself establish run-ahead readiness: that has its separate gate below.
|
||||
Uint64 ConsumedSubsystemsFor(BackendType type) {
|
||||
switch (type) {
|
||||
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e;
|
||||
case BackendType::DirectVulkan:
|
||||
return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// THE CROSS-CHECK THAT MAKES A WRONG ANSWER LOUD. Claiming bit 7 while no resource op
|
||||
// table is registered is the exact failure the mask exists to prevent, one level down:
|
||||
// the client would keep emitting the resource family and the server would drop every
|
||||
// record of it. The check runs AFTER Initialize(), which is where DirectGLES registers
|
||||
// the table, so it can see the real answer rather than a promise.
|
||||
void AssertConsumerMaskIsHonest(Uint64 mask) {
|
||||
const Bool claimsResources = (mask & MG_Pipe::kMGPipeSubsystemResources) != 0;
|
||||
const Bool hasResourceOps = MG_Pipe::MGPipeGetResourceOps() != nullptr;
|
||||
if (claimsResources && !hasResourceOps) {
|
||||
MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"kMGPipeSubsystemResources\"} - the "
|
||||
"server published a consumer bit for the resource family while "
|
||||
"MGPipeGetResourceOps() is null. The client's R-8 liveness gate would "
|
||||
"keep emitting resource_create / resource_subdata records that this "
|
||||
"server drops on the floor, and the client clears its dirty flags on "
|
||||
"acceptance anyway (ID-39). A mask is a statement about this backend, "
|
||||
"not a hope");
|
||||
std::abort();
|
||||
}
|
||||
if (!claimsResources && hasResourceOps) {
|
||||
// The safe direction: the legacy pull path keeps running. Said out loud anyway,
|
||||
// because it silently costs the whole P3a family its migration.
|
||||
MGLOG_W("MG_Remote server: a resource op table is registered but the consumer "
|
||||
"mask withholds kMGPipeSubsystemResources; the buffer family will fall "
|
||||
"back to the legacy path for this session");
|
||||
}
|
||||
}
|
||||
|
||||
// The resource op table the SERVER's backend registered at step 1 (BackendObject_DirectGLES::
|
||||
// Initialize -> RegisterBufferBackendOps), as step 2 saw it. Step 5 compares against it
|
||||
// (review v2 N-8): a client object that registered a table of its own would have made
|
||||
// AssertConsumerMaskIsHonest's "is a table registered" answer TRUE, so re-asking that
|
||||
// question could never notice the swap - only the pointer can.
|
||||
const MG_Pipe::MGPipeResourceOps* g_resourceOpsAtStep2 = nullptr;
|
||||
|
||||
// The single hook (ARCHITECTURE.md:29). Returns false when the split could not be
|
||||
// brought up, and the caller then REFUSES TO CONTINUE rather than falling back to the
|
||||
// switch below - a fallback here is "the split lane ran monolith and went green".
|
||||
// Steps 1 and 2 of the split bring-up: the server role's private backend
|
||||
// object and the two CallMask halves. EXTRACTED so the spawn server
|
||||
// process runs the SAME code the inproc server role runs - a second copy
|
||||
// is how the two roles end up disagreeing about a capability bit that
|
||||
// only one of them ever computes. ServerMain calls this and then does
|
||||
// its own Accept; InitSplitRoles calls it and then starts the client.
|
||||
Bool InitServerRoleCommon() {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
|
||||
// 0. the Magma wire funnels' route to Session::Fail (P7 wave 0). FIRST, before
|
||||
// CreateBackend can reach a wire arm: a hook installed after the first death it
|
||||
// was meant to catch is a hook that does nothing on the only run that mattered.
|
||||
// Here rather than in ServerMain because BOTH server roles pass through this
|
||||
// function - the inproc role and the spawn/TCP session child - and a per-transport
|
||||
// install is how one of the two ends up without it.
|
||||
InstallPipeSessionFailHook();
|
||||
|
||||
// 1. the SERVER role's private backend object, on the app thread, with no GL and no
|
||||
// EGL. The context is created and made current later, on mgl-srv-apply, when the
|
||||
// client's first eglMakeCurrent crosses as a blocking control request.
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
const MobileGLResult created = loop.CreateBackend(MG_Config::ActiveBackendType);
|
||||
if (created != MOBILEGL_OK) return false;
|
||||
|
||||
// 2. the two CallMask halves. NEITHER HAS A DEFAULT and CallMask() is a named Fatal
|
||||
// on an unset one (s1's BLOCKER fix), so this is the "somebody" that block names.
|
||||
Server::ServerSession& session = Server::ServerSessionInstance();
|
||||
const Uint64 consumed = ConsumedSubsystemsFor(MG_Config::ActiveBackendType);
|
||||
AssertConsumerMaskIsHonest(consumed);
|
||||
g_resourceOpsAtStep2 = MG_Pipe::MGPipeGetResourceOps();
|
||||
session.SetConsumedSubsystems(consumed);
|
||||
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
|
||||
// every optional capability bit belongs to the package that owns its question, and
|
||||
// withholding one leaves the legacy path running, which is the safe direction.
|
||||
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
||||
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
||||
// what keeps every one of them out of the first IPC frame (contract table 0).
|
||||
//
|
||||
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
|
||||
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
|
||||
// SERVER's backend own the transform-feedback capture", and the server's table is
|
||||
// visible here and nowhere on the client. It is read straight off the table
|
||||
// ServerLoop::CreateBackend just built - Espryt registers XfbImpl::EndTransformFeedback
|
||||
// (BackendObject_DirectGLES.cpp:1458) and Magma registers no XFB slot at all - so the
|
||||
// bit is a statement about THIS backend rather than about a build option, which is
|
||||
// what makes it survive a backend switch. The client reads it through
|
||||
// MGL_BACKEND_SLOT_CAP at GL_Drawing.cpp's FixupGsStripCaptureOrder.
|
||||
Uint64 capBits = 0;
|
||||
if (const MG_Backend::BackendObject* serverBackend = loop.Backend();
|
||||
serverBackend != nullptr &&
|
||||
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
|
||||
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
|
||||
}
|
||||
if (const auto* backend = loop.Backend()) {
|
||||
const auto& gl = backend->GetBackendFunctions().GL;
|
||||
// The query owner publishes only complete native query/reply paths.
|
||||
// Timer hardware support is refreshed when caps are published after
|
||||
// make-current; no CPU primitive-accounting preference is advertised.
|
||||
if (gl.IsQueryResultAvailable && gl.GetQueryResult64 && gl.DeleteBackendQuery &&
|
||||
gl.FenceSync && gl.ClientWaitSync && gl.DeleteSync) {
|
||||
if (gl.BeginXfbPrimitivesQuery && gl.EndXfbPrimitivesQuery)
|
||||
capBits |= MG_Pipe::kCapXfbPrimitivesQuery;
|
||||
if (gl.BeginOcclusionQuery && gl.EndOcclusionQuery)
|
||||
capBits |= MG_Pipe::kCapOcclusionQuery;
|
||||
if (gl.IsTimerQuerySupported && gl.BeginTimeElapsedQuery &&
|
||||
gl.EndTimeElapsedQuery && gl.QueryCounterTimestamp)
|
||||
capBits |= MG_Pipe::kCapTimerQuery;
|
||||
}
|
||||
}
|
||||
// P7 wave 2 package C, OQ-10 (CONTRACT-P7 §5.4): kCapResidentSubData, PUBLISHED
|
||||
// OFF THE SERVER'S OWN WIRE RESOURCE TABLE AND NOTHING ELSE.
|
||||
//
|
||||
// The question the bit answers is the one BufferObject::LandBytesIntoResidentStore
|
||||
// asks before it chooses between emitting `buffer_subdata_resident` (opcode 49) and
|
||||
// falling back to the ordered in-place host memcpy: "does the side that will APPLY
|
||||
// this record implement the resident arm at all?" Under a transport the client has
|
||||
// no op table to probe - MGPipeResourceOpsHaveSubDataResident says so in as many
|
||||
// words (MG_Impl/Pipe/PipeFill.cpp) and reads this bit instead - and until now
|
||||
// nothing ever set it, so the answer was permanently "no" and BOTH backends fell
|
||||
// back to the memcpy while both of them in fact register a SubDataResident arm
|
||||
// (DirectGLES Managers.cpp's g_glesResourceOps, Magma VkBufferManager.cpp's
|
||||
// g_vulkanWireResourceOps). Magma's implementation was dead code on the wire.
|
||||
//
|
||||
// BY THE TABLE, NEVER BY THE BACKEND ENUM, and that is ID-39's lesson rather than a
|
||||
// style preference: a mask is a statement about THIS backend's registered table, so
|
||||
// a bit derived from `ActiveBackendType == ...` would keep claiming the capability
|
||||
// for a build, a flavour or a future backend whose table does not carry the arm -
|
||||
// which is exactly how 66 uploads were lost to a consumer bit that nobody could
|
||||
// have honoured. `g_resourceOpsAtStep2` is the pointer this function just pinned,
|
||||
// and step 5 already refuses a run where that pointer was swapped underneath us, so
|
||||
// reading the arm off it means the published bit and the table that has to answer
|
||||
// for it cannot come apart.
|
||||
if (g_resourceOpsAtStep2 != nullptr && g_resourceOpsAtStep2->SubDataResident != nullptr) {
|
||||
capBits |= MG_Pipe::kCapResidentSubData;
|
||||
}
|
||||
// Each backend has an independent implementation-readiness gate.
|
||||
// The runtime RunAhead knob can decline the feature, never create it.
|
||||
const Bool runAheadReady = MG_Config::ActiveBackendType == BackendType::DirectVulkan
|
||||
? kMGPipeMagmaRunAheadReady : kMGPipeP5eRunAheadReady;
|
||||
capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType, runAheadReady);
|
||||
session.SetCapabilityBits(capBits);
|
||||
session.SetBackend(loop.Backend());
|
||||
return true;
|
||||
}
|
||||
|
||||
// The single hook (ARCHITECTURE.md:29) for the INPROC shape: both roles in
|
||||
// this process. The spawn shape runs InitServerRoleCommon in the child and
|
||||
// the client half here.
|
||||
Bool InitSplitRoles() {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
|
||||
// SPAWN: THE SERVER ROLE IS NOT IN THIS PROCESS. Steps 1 and 2 -
|
||||
// the backend and the two CallMask halves - happen in the server,
|
||||
// which ran InitServerRoleForSpawn before it accepted us. Running
|
||||
// them here as well would build a second BackendObject in the one
|
||||
// process that must not have one, and would publish a capability
|
||||
// mask computed from the WRONG backend's function table.
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Spawn) {
|
||||
const MobileGLResult started = Client::ClientSessionInstance().StartSpawned();
|
||||
if (started != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote: the spawn session failed to start (rc=%d); MobileGL will "
|
||||
"NOT fall back to monolith - a lane named split that ran monolith is "
|
||||
"the one failure this phase is built to make impossible",
|
||||
static_cast<int>(started));
|
||||
return false;
|
||||
}
|
||||
pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!InitServerRoleCommon()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. the handshake, the four segments, and - at its end - the apply thread.
|
||||
const MobileGLResult started =
|
||||
Client::ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint);
|
||||
if (started != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote: the split session failed to start (rc=%d); MobileGL will not "
|
||||
"fall back to monolith - a lane named split that ran monolith is the one "
|
||||
"failure this phase is built to make impossible",
|
||||
static_cast<int>(started));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. and only now the CLIENT's backend object in the one global that holds it.
|
||||
// Table 3: pActiveBackendObject holds BackendObject_Remote and the server's
|
||||
// BackendObject_DirectGLES stays private to ServerLoop.
|
||||
pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
|
||||
return true;
|
||||
}
|
||||
|
||||
// F1 (P7 wave 2). THE WHOLE SPLIT ARM OF Init(), AS ONE BODY, so it can run from
|
||||
// either of the two places that may reach it - MobileGL::Initialize's early hook,
|
||||
// before MG_State::Init(), and Init() itself for a direct caller such as a unit
|
||||
// fixture - without a second copy. Two copies of a bring-up order is how the two
|
||||
// orders come to differ on the day one of them is wrong.
|
||||
Bool g_splitBackendBroughtUp = false;
|
||||
|
||||
void InitSplitBackend() {
|
||||
g_splitBackendBroughtUp = true;
|
||||
if (!InitSplitRoles()) {
|
||||
// NOT a fallback to the switch. pActiveBackendObject stays null and the next GL
|
||||
// call fails loudly, which is the only honest outcome: the operator asked for a
|
||||
// transport this process could not bring up.
|
||||
pActiveBackendObject = nullptr;
|
||||
return;
|
||||
}
|
||||
Bool remoteResult = InitSpecificBackendLibs();
|
||||
if (!remoteResult) {
|
||||
MGLOG_W("Failed to initialize MobileGL backend libraries for the remote object");
|
||||
return;
|
||||
}
|
||||
// P6: BOTH CHECKS BELOW ARE STATEMENTS ABOUT THE SERVER'S PROCESS, and under
|
||||
// spawn that is not this one. Their own wording says so - "a mask is a
|
||||
// statement about THIS BACKEND", "the table the SERVER'S BACKEND registered at
|
||||
// step 1" - and in a spawn client there is no backend and never was a table, so
|
||||
// MGPipeGetResourceOps() is null by construction and the first check fires on a
|
||||
// correct session. It did, on the first spawn retrace ever run.
|
||||
//
|
||||
// The checks are NOT weakened: the server runs the step-2 one itself, inside
|
||||
// InitServerRoleForSpawn, against its own freshly registered table - which is the
|
||||
// process where the question has an answer. What is lost under spawn is the
|
||||
// step-5 re-run's ability to catch a table REPLACED between accept and here, and
|
||||
// that is a question about one address space; it cannot be asked across two.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// m-6, re-worded per review v2 N-8. The honesty cross-check runs a SECOND time, now
|
||||
// that step 4's pActiveBackendObject (the client's BackendObject_Remote) exists and
|
||||
// its Initialize() has run inside InitSpecificBackendLibs. What the re-run CAN catch
|
||||
// is a table that was REMOVED between step 2 and here (the claim would then be a lie
|
||||
// again). What it cannot catch - and its first comment claimed it could - is a client
|
||||
// object that REGISTERED a table of its own: that leaves "is a table registered"
|
||||
// true. Only the pointer tells those apart, so the table is compared against the one
|
||||
// step 2 saw and a swap is refused by name: the applier would otherwise dispatch the
|
||||
// server's resource records into the CLIENT object's table under its feet.
|
||||
AssertConsumerMaskIsHonest(ConsumedSubsystemsFor(MG_Config::ActiveBackendType));
|
||||
if (MG_Pipe::MGPipeGetResourceOps() != g_resourceOpsAtStep2) {
|
||||
MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"resource ops table replaced\"} - the "
|
||||
"resource op table MGPipeGetResourceOps() answers with is not the one the "
|
||||
"server's backend registered at step 1 (%p now, %p then). Something between "
|
||||
"ServerSession::Accept and the client object's Initialize() registered its "
|
||||
"own table, and the applier would dispatch every resource record into it. A "
|
||||
"mask is a statement about the server's backend, and so is the table",
|
||||
static_cast<const void*>(MG_Pipe::MGPipeGetResourceOps()),
|
||||
static_cast<const void*>(g_resourceOpsAtStep2));
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
LogBackendInfo();
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// F1 (P7 wave 2). See BackendObjects.h for why this exists and MobileGL/Init.cpp for the
|
||||
// defect the order closes.
|
||||
Bool InitSplitRolesBeforeState() {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
|
||||
InitSplitBackend();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void ShutdownSplitRoles() {
|
||||
g_splitBackendBroughtUp = false;
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// ClientSession::Stop IS table 3's whole order and it is idempotent: publish and wait
|
||||
// for the server to drain (bounded - a lost record must be a red lane, not a hung
|
||||
// exit), Doorbell::Kill through the transport's Shutdown, ServerLoop::Stop's bounded
|
||||
// join - which also destroys the server's private BackendObject ON the apply thread
|
||||
// while it still owns the context - the transport, and only THEN anything an emitter
|
||||
// owns. A var-tail still named by an unapplied record is a use-after-free the join is
|
||||
// what prevents, which is why the order is not a preference.
|
||||
MG_Remote::Client::ClientSessionInstance().Stop();
|
||||
// M-6: ClientSession::Stop's !m_started arm (a Start that FAILED after
|
||||
// ServerSession::Accept - a refused Accept, an invalid cmd/reply ring) tears down only
|
||||
// the client half and never stops the apply thread or drops the server's private backend,
|
||||
// which ServerLoop::CreateBackend already built and which holds the process-wide
|
||||
// g_resourceOps. So call ServerLoop::Stop() here unconditionally. It is idempotent: on the
|
||||
// started path ClientSession::Stop already joined the thread, so this hits Stop's
|
||||
// !joinable arm, which resets a backend that never ran a thread and is otherwise a no-op.
|
||||
// Without this an early Start failure leaves BackendObject_DirectGLES permanently alive
|
||||
// and every later split bring-up in the process fails at CreateBackend's m_backend!=null
|
||||
// guard.
|
||||
MG_Remote::Server::ServerLoopInstance().Stop();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The spawn server's entry into the SAME bring-up the inproc server role
|
||||
// runs (ServerRole.h). A thin forwarder on purpose: the body stays in the
|
||||
// anonymous namespace beside InitSplitRoles so the two cannot drift.
|
||||
//
|
||||
// THE WHOLE FUNCTION IS INSIDE THE GUARD, not just its body. A version with
|
||||
// the guard inside still DEFINES the symbol in a pull build, and G1 caught
|
||||
// it: 2 symbols added, 1 removed, .text +16 bytes. The pull build's identity
|
||||
// is byte-for-byte, and "it returns false there" is not the same as "it is
|
||||
// not there".
|
||||
Bool InitServerRoleForSpawn() { return InitServerRoleCommon(); }
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Initializing MobileGL Backend...");
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE SINGLE HOOK. In a build without MOBILEGL_BUILD_DISAGGREGATED, MG_Config::Transport
|
||||
// is a `constexpr Monolith` (Config.h) and this whole statement is discarded, so the
|
||||
// pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// F1: MobileGL::Initialize now brings the split roles up BEFORE MG_State::Init(),
|
||||
// through InitSplitRolesBeforeState() below, so by the time Init() is reached on
|
||||
// that path the work is done. A DIRECT caller - a unit fixture that calls
|
||||
// MG_Backend::Init() itself - still gets the whole bring-up here. The flag rather
|
||||
// than `pActiveBackendObject == nullptr`, because a bring-up that FAILED leaves
|
||||
// that pointer null too and retrying it would refuse at CreateBackend's
|
||||
// m_backend != nullptr guard with a second, misleading line.
|
||||
if (!g_splitBackendBroughtUp) InitSplitBackend();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (MG_Config::ActiveBackendType) {
|
||||
case BackendType::DirectGLES:
|
||||
pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>();
|
||||
|
||||
@@ -1,715 +0,0 @@
|
||||
// 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>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#endif
|
||||
|
||||
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_BUILD_DISAGGREGATED
|
||||
// ================================================================================
|
||||
// P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict
|
||||
// ================================================================================
|
||||
namespace {
|
||||
Uint64 g_residualPulls = 0;
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// ---- P7 wave 3 (V1): negative control B's SERVER half ----------------------------
|
||||
//
|
||||
// MOBILEGL_PIPE_POISON_OMIT withholds the STAMP of one (verb, field) pair, and until a
|
||||
// verify build first ran on the split arm it was only ever read by the client's filler
|
||||
// (MG_Impl/Pipe/PipeFill.cpp's ParsePoisonOmissionKnob). On the split arm that is not
|
||||
// where the stamp the backend reads comes from: the stamp below re-stamps every field
|
||||
// the verb's class can answer out of applied records, so it restamped the very field
|
||||
// the client had withheld and the control went GREEN - measured, both backends, the
|
||||
// child's glGenerateMipmap returned and the process exited 0 with
|
||||
// GenerateMipmap:GetActiveTextureUnit omitted. The knob means "this verb's stamp of this
|
||||
// field was never written", and under a transport the stamp that answers the backend is
|
||||
// this one, so this one honours it too.
|
||||
//
|
||||
// PARSED HERE, NOT SHARED WITH THE CLIENT'S PARSER, because this file is in the server
|
||||
// library and MG_Impl is not. A malformed value is the client's Fatal{PipeVerifyBadKnob}
|
||||
// (it parses first, at the first fill of the process); this side simply does not arm.
|
||||
// VERIFY BUILDS ONLY: the knob belongs to the verify harness, and a push build's stamp
|
||||
// stays the branch-free fill the P5d profile asked for.
|
||||
struct ServerPoisonOmission {
|
||||
Bool Parsed = false;
|
||||
String Value;
|
||||
Bool Armed = false;
|
||||
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
|
||||
MGPipeInputField Field = MGPipeInputField::kFieldCount;
|
||||
};
|
||||
ServerPoisonOmission g_serverOmission;
|
||||
|
||||
const ServerPoisonOmission& ServerOmission() {
|
||||
const String& knob = MG_Config::Features.PipePoisonOmit;
|
||||
if (g_serverOmission.Parsed && knob.size() == g_serverOmission.Value.size() &&
|
||||
(knob.empty() || knob == g_serverOmission.Value)) {
|
||||
return g_serverOmission;
|
||||
}
|
||||
g_serverOmission = ServerPoisonOmission{};
|
||||
g_serverOmission.Parsed = true;
|
||||
g_serverOmission.Value = knob;
|
||||
const auto colon = knob.find(':');
|
||||
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) return g_serverOmission;
|
||||
const auto verb = MGPipeFindVerb(knob.substr(0, colon).c_str());
|
||||
const auto field = MGPipeFindInputField(knob.substr(colon + 1).c_str());
|
||||
if (!verb || !field) return g_serverOmission;
|
||||
g_serverOmission.Armed = true;
|
||||
g_serverOmission.Verb = *verb;
|
||||
g_serverOmission.Field = *field;
|
||||
return g_serverOmission;
|
||||
}
|
||||
|
||||
Bool ServerOmitsStamp(MGPipeVerb verb, MGPipeInputField field) {
|
||||
const ServerPoisonOmission& omission = ServerOmission();
|
||||
return omission.Armed && omission.Verb == verb && omission.Field == field;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
|
||||
// The strict arm of R-7.3. Same first line as the ordinary poison Fatal, so every
|
||||
// existing filter on Fatal{UnmigratedPipeInput still matches, plus the class and the
|
||||
// phase that retires it - a strict abort that did not say which phase owes the answer
|
||||
// would leave the reader exactly where the gate found them.
|
||||
// P5e (ra): `why` is the second half of the marker, because the lane's allowlist step
|
||||
// reads this line and the two reasons mean different things to it. "MOBILEGL_IPC_
|
||||
// STRICT_ERRORS=1" is the operator asking for a real, ordered value to be loud;
|
||||
// "UNBARRIERED" is the value not existing (§3.3). The `<field>@<verb>` prefix and the
|
||||
// Fatal{UnmigratedPipeInput tag are unchanged, so every filter written since P5c still
|
||||
// matches.
|
||||
[[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb,
|
||||
const char* why) {
|
||||
const SizeT index = static_cast<SizeT>(field);
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, %s, "
|
||||
"retires in %s]",
|
||||
kMGPipeInputFieldNames[index], MGPipeVerbName(verb), why,
|
||||
kMGPipeFieldRetiringPhase[index]);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// P5e (gl), ID-117: THE ADMITTED MARKER, AND WHY IT IS ONLY THE TAG THAT CHANGED.
|
||||
//
|
||||
// `<field>@<verb>` and the `[BARRIER-PULLED, …, retires in <phase>]` tail are IDENTICAL
|
||||
// to the Fatal above; the leading tag is `Admitted{` instead of `Fatal{`. Every filter
|
||||
// written since P5c matches `Fatal{UnmigratedPipeInput`, so all of them keep meaning
|
||||
// exactly "red" and none of them has to learn a new grammar to keep meaning it. A reader
|
||||
// grepping for the pair still finds it, and the lane can ratchet on both sets with one
|
||||
// regex per tag (ID-119's two-sided ratchet).
|
||||
//
|
||||
// THE DEDUPE IS NOT TIDINESS. An 852-draw Minecraft frame reaches an admitted readback
|
||||
// row once per draw; without this, one frame writes 852 identical lines, the log file is
|
||||
// the size of the run and the marker census cannot be read at all. So it is once per
|
||||
// (field, verb) per process, which is the granularity the allowlist is written in.
|
||||
//
|
||||
// A PLAIN ARRAY, NOT AN ATOMIC, and that is the file's existing rule rather than a
|
||||
// shortcut: CountBarrierPull is reachable only after the server stamped a verb boundary,
|
||||
// which happens on the apply thread inside PipeApplier::ApplyOne, and g_residualPulls
|
||||
// beside it is a plain Uint64 for the same reason. A racing writer here would at worst
|
||||
// log a duplicate line, never lose one.
|
||||
void AdmittedBarrierPullOnce(MGPipeInputField field, MGPipeVerb verb, Bool escalated) {
|
||||
const SizeT fieldIndex = static_cast<SizeT>(field);
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (fieldIndex >= kMGPipeInputFieldCount || verbIndex >= kMGPipeVerbCount) return;
|
||||
static Uint64 seen[(kMGPipeVerbCount * kMGPipeInputFieldCount + 63) / 64] = {};
|
||||
const SizeT bit = verbIndex * kMGPipeInputFieldCount + fieldIndex;
|
||||
const Uint64 mask = Uint64{1} << (bit % 64);
|
||||
if ((seen[bit / 64] & mask) != 0) return;
|
||||
seen[bit / 64] |= mask;
|
||||
// P5e (gl), ID-128: WHICH DISJUNCT ADMITTED IT, in the slot the grammar already has
|
||||
// for the reason. `ADMITTED` means the generated table said so, and the lane can
|
||||
// check that against `--print-admitted`. `ADMITTED-ESCALATED` means the table did
|
||||
// NOT and the record was barriered by an escalation the table cannot see, which is a
|
||||
// RUNTIME fact about that record's payload - so the lane must not look for it in a
|
||||
// static list. Saying which is what keeps the lane's comparison exact instead of
|
||||
// widening the list with every pair that could ever escalate.
|
||||
MGLOG_W("MGPipe: Admitted{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, %s, "
|
||||
"retires in %s]",
|
||||
kMGPipeInputFieldNames[fieldIndex], MGPipeVerbName(verb),
|
||||
escalated ? "ADMITTED-ESCALATED" : "ADMITTED",
|
||||
kMGPipeFieldRetiringPhase[fieldIndex]);
|
||||
}
|
||||
|
||||
// One place decides what a BARRIER-PULLED read does, so the field accessors and the
|
||||
// seven sticky forwards cannot drift apart on it.
|
||||
//
|
||||
// ---- P5e (ra), CONTRACT-P5E §3.3: THE DETECTOR IS UNCONDITIONAL UNDER AN UNBARRIERED
|
||||
// RECORD, AND THAT IS WHAT MAKES THE STRICT LANE A GATE -------------------------------
|
||||
//
|
||||
// The knob exists because under LOCKSTEP a pulled row is a real, ordered, fresh value:
|
||||
// the client filled it and then parked, so "count it and carry on" is an honest
|
||||
// measurement of remaining debt and MOBILEGL_IPC_STRICT_ERRORS is the operator asking
|
||||
// for the debt to be loud instead. None of that survives run-ahead. With the client
|
||||
// running ahead of this apply, the row was either never filled for this verb (§3.1
|
||||
// skips the fill) or is being overwritten by a verb two frames later - so the value is
|
||||
// torn or stale BY CONSTRUCTION and there is nothing for a counter to count. A
|
||||
// "count it" arm here would be a wrong picture with a number beside it.
|
||||
//
|
||||
// P5e (gl), ID-119: THE OLD PIN HERE - "rsp is 0 on unbarriered records" - WAS VACUOUS
|
||||
// AND HAS BEEN WITHDRAWN. The unbarriered arm below is [[noreturn]] and runs BEFORE
|
||||
// ++g_residualPulls, so an unbarriered pull can never reach the counter whatever this
|
||||
// function is written to do; the sentence was true of every possible implementation and
|
||||
// therefore checked nothing. What rsp actually counts is BARRIERED pulls, and under the
|
||||
// strict knob it counts exactly the ADMITTED ones, because every other barriered pull
|
||||
// aborts two lines down. That is the form CONTRACT-P5E §7 now states and the lane checks.
|
||||
//
|
||||
// ---- P5e (gl), ID-117: AN ADMITTED PULL IS LOUD, NOT FATAL ---------------------------
|
||||
//
|
||||
// Before this, strict aborted on EVERY barrier-pulled read, admitted or not - which made
|
||||
// the CI's allowlist comparison unreachable code (the run's rc != 0 exits first) and made
|
||||
// "hard green" impossible with the knob as written. So there is a third state, and the
|
||||
// two existing ones are untouched:
|
||||
//
|
||||
// unbarriered -> Fatal, no knob. The value is torn by construction.
|
||||
// barriered, unadmitted -> Fatal under strict. A debt no phase has taken.
|
||||
// barriered, admitted -> ONE MGLOG_W per (field, verb), and the entry completes.
|
||||
//
|
||||
// "Admitted" has three disjuncts and the third is a runtime one (ID-128): the generated
|
||||
// table answers the first two, and the record's own escalation flag answers the third.
|
||||
//
|
||||
// An admitted pull is a debt this phase deliberately leaves standing: the field's row is
|
||||
// BARRIER_PULLED, the verb's op is statically barriered so the client really is parked
|
||||
// behind the record, and the field is inside the verb's own may-read mask - so the value
|
||||
// is real, ordered and fresh, exactly ruling 4's "barriered records keep P5C semantics".
|
||||
// MGPipeBarrierPullAdmitted is generated from those three tables (ID-116); there is no
|
||||
// list here to drift.
|
||||
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: UNDER THE DUAL-BLOCK REHEARSAL EVERY
|
||||
// BARRIER-PULLED READ IS A NAMED FATAL, unconditionally - ahead of the barriered
|
||||
// question, ahead of the strict knob, ahead of the counter. The server block has no
|
||||
// residual fill to answer from, so the value the read would return is not "torn"
|
||||
// or "stale" but ABSENT: the field's stamp was withdrawn by the server's own verb
|
||||
// boundary and nothing on this side of the role split ever writes it. Letting the
|
||||
// read proceed would hand the backend default storage - or, for the O-class rows
|
||||
// with no null check, an empty SharedPtr to dereference - which is an UNNAMED
|
||||
// crash exactly where the rehearsal exists to produce a named one. The red is the
|
||||
// census (Harness/dualblock-expected-fatals.txt), so the marker keeps the strict
|
||||
// grammar with the knob's own name as the reason.
|
||||
// D10: rides MGPipeBlocksAreDistinct() - under spawn the two blocks are distinct
|
||||
// whatever the rehearsal knob says, and a barrier pull across a real process
|
||||
// boundary is exactly the thing this Fatal exists to name.
|
||||
if (MGPipeBlocksAreDistinct()) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_ROLE_SPLIT_STATE=1");
|
||||
}
|
||||
if (!MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
|
||||
}
|
||||
++g_residualPulls;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
|
||||
}
|
||||
if (MG_Config::Ipc.StrictErrors) {
|
||||
// THE THIRD DISJUNCT (P5e gl, ID-128), and it has to be asked at RUNTIME because
|
||||
// it is a fact about this record's payload rather than about its opcode. The
|
||||
// static table admits a pair when the verb's op waits (disjunct 1) or when the
|
||||
// field's retiring phase is not this phase (disjunct 2); an escalated record is
|
||||
// one the client parks behind for a reason only the payload knows - an open
|
||||
// transform-feedback span, or a draw carrying client vertex arrays. Both are
|
||||
// outside this phase by ruling (§5.7, ID-82), and the client-array read site
|
||||
// aborts by its own name if it is ever applied unbarriered, so the pull is legal
|
||||
// and P5e is not the phase that owes it.
|
||||
const Bool statically = MGPipeBarrierPullAdmitted(field, verb);
|
||||
const Bool escalated = MGPipeApplierCurrentRecordIsBarrieredByEscalation();
|
||||
if (!statically && !escalated) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_STRICT_ERRORS=1");
|
||||
}
|
||||
AdmittedBarrierPullOnce(field, verb, !statically);
|
||||
}
|
||||
}
|
||||
|
||||
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
|
||||
// respects it for the same reason the client's residual fill does: a field outside the
|
||||
// verb's class is one the fill never copied, so answering it out of gPipeInputs would
|
||||
// hand the server the PREVIOUS verb's value - the exact staleness the generation poison
|
||||
// exists to catch, re-introduced by the very mechanism meant to instrument it.
|
||||
Bool FieldIsInVerbClass(MGPipeInputField field, MGPipeVerb verb) {
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (verbIndex >= kMGPipeVerbCount) return false;
|
||||
const MGPipeVerbClass verbClass = kMGPipeVerbClass[verbIndex];
|
||||
return MGPipeFieldMaskHas(kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)], field);
|
||||
}
|
||||
|
||||
// ---- the same verdict, per verb CLASS, as a constant (P5d round 3, package C) ----
|
||||
//
|
||||
// THE STAMP's ANSWER IS A CONSTANT OF THE VERB CLASS AND OF NOTHING ELSE. Both halves of
|
||||
// `answerable` below read constexpr tables only - kMGPipeFieldOwnership, kMGPipeVerbClass
|
||||
// and kMGPipeClassFieldMask - so the 63-field loop was recomputing, at every verb on the
|
||||
// apply thread, a table the compiler can build once. The 2026-09-17 inproc profile put
|
||||
// MGPipeServerStampVerbBoundary at 1.1% self of the apply thread (Minecraft 26.3-rc-3,
|
||||
// ~852 draws/frame), and every cycle taken there is a cycle the lockstep client waits for.
|
||||
//
|
||||
// IT IS A MASK, NOT A BOOL ARRAY, so the stamp loop's body stays a shift and a store with
|
||||
// no branch: FilledGen[i] = serial & -bit, which is `serial` for an answerable field and
|
||||
// the WITHDRAWAL 0 for every other. The verdict itself is unchanged, and it is still
|
||||
// computed by the one expression argued at the stamp - it just runs at compile time.
|
||||
constexpr MGPipeFieldMask AnswerableMaskForClass(MGPipeVerbClass verbClass) {
|
||||
MGPipeFieldMask mask{};
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[i];
|
||||
const Bool answerable = (ownership == MGPipeFieldOwnership::kRecordSupplied ||
|
||||
ownership == MGPipeFieldOwnership::kApplierDerived) &&
|
||||
MGPipeFieldMaskHas(
|
||||
kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)], field);
|
||||
if (answerable) mask.Words[i / 64] |= (Uint64{1} << (i % 64));
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
struct AnswerableMaskTable {
|
||||
MGPipeFieldMask ByClass[kMGPipeVerbClassCount];
|
||||
};
|
||||
|
||||
constexpr AnswerableMaskTable MakeAnswerableMaskTable() {
|
||||
AnswerableMaskTable table{};
|
||||
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) {
|
||||
table.ByClass[i] = AnswerableMaskForClass(static_cast<MGPipeVerbClass>(i));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
constexpr AnswerableMaskTable kAnswerableByClass = MakeAnswerableMaskTable();
|
||||
|
||||
// A verb id outside the table answers NOTHING, which is exactly what FieldIsInVerbClass
|
||||
// said for it (`verbIndex >= kMGPipeVerbCount` -> false for every field) and therefore
|
||||
// what the stamp said: all 63 withdrawn, every read the poison Fatal.
|
||||
constexpr MGPipeFieldMask kNoFieldIsAnswerable{};
|
||||
|
||||
const MGPipeFieldMask& AnswerableMaskForVerb(MGPipeVerb verb) {
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (verbIndex >= kMGPipeVerbCount) return kNoFieldIsAnswerable;
|
||||
return kAnswerableByClass.ByClass[static_cast<SizeT>(kMGPipeVerbClass[verbIndex])];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The third door into the storage (see PipeInputs.h). It exists because neither of the
|
||||
// other two can be the one that stamps: MGPipeApplyAccess deliberately does not, and
|
||||
// MGPipeFillAccess lives in MG_Impl, the role a server does not have.
|
||||
struct MGPipeStampAccess {
|
||||
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
|
||||
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
|
||||
static void SetServerStamped(PipeInputs& inputs, Bool stamped) {
|
||||
inputs.m_serverStampedVerb = stamped;
|
||||
}
|
||||
// P5f (f1): the server block's identity half of the stamp door (CONTRACT-P5E §3.2).
|
||||
// The client's SetIdentity lives in MG_Impl's MGPipeFillAccess and writes the CLIENT
|
||||
// block now; this one is how the server block learns which served context it describes
|
||||
// without MG_Impl being in the process at all.
|
||||
static void SetIdentity(PipeInputs& inputs, Bool live, const void* identity) {
|
||||
inputs.m_live = live;
|
||||
inputs.m_contextIdentity = identity;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- P5f (f1): the dual-block rehearsal's selection functions (PipeInputs.h) -----------
|
||||
//
|
||||
// MGPipeRoleSplitRehearsalActive is deliberately NOT latched: it is two global loads, asked once per
|
||||
// verb on the fill side and once per verb boundary on the stamp side, and a latch is a
|
||||
// second thing a test that flips the knob mid-process would have to know about.
|
||||
Bool MGPipeRoleSplitRehearsalActive() {
|
||||
return MG_Config::Ipc.RoleSplitState &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- D1c's predicates (CONTRACT-P6 3.2) -------------------------------------------------
|
||||
namespace {
|
||||
// Set once by ServerMain, before any GL work. A spawn server is the server for the whole
|
||||
// life of the process; there is no thread in it that is not.
|
||||
Bool g_serverProcessRole = false;
|
||||
// MG_Remote's "is the calling thread inside the applier". Null until a session installs
|
||||
// it, which is the honest answer for a process that has no applier.
|
||||
Bool (*g_applyThreadProbe)() = nullptr;
|
||||
Bool g_sessionLive = false;
|
||||
} // namespace
|
||||
|
||||
void MGPipeSetServerProcessRole(Bool isServerProcess) { g_serverProcessRole = isServerProcess; }
|
||||
void MGPipeSetApplyThreadProbe(Bool (*probe)()) { g_applyThreadProbe = probe; }
|
||||
void MGPipeSetSessionLive(Bool live) { g_sessionLive = live; }
|
||||
|
||||
Bool MGPipeServerArm() {
|
||||
// THE PROCESS FACT FIRST, and it short-circuits: in a spawn server every thread is a
|
||||
// server thread, and asking the probe would be asking MG_Remote a question it answers
|
||||
// only about the applier.
|
||||
if (g_serverProcessRole) return true;
|
||||
return g_applyThreadProbe != nullptr && g_applyThreadProbe();
|
||||
}
|
||||
|
||||
Bool MGPipeSessionLive() { return g_sessionLive; }
|
||||
|
||||
Bool MGPipeBlocksAreDistinct() {
|
||||
// TWO DIFFERENT REASONS, ONE ANSWER. The rehearsal makes two blocks in one process;
|
||||
// spawn makes them two blocks in two ADDRESS SPACES, where no knob is involved and the
|
||||
// distinctness is a fact about the machine. The guards that used to ask only the first
|
||||
// question were therefore disarmed in the one shape where the answer matters most - a
|
||||
// spawn server's block is never the client's, whatever MOBILEGL_IPC_ROLE_SPLIT_STATE
|
||||
// says.
|
||||
return MGPipeRoleSplitRehearsalActive() ||
|
||||
MG_Config::Transport == MG_Config::TransportMode::Spawn;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
PipeInputs& MGPipeClientInputs() {
|
||||
return MGPipeRoleSplitRehearsalActive() ? gPipeInputsClientBlock : gPipeInputs;
|
||||
}
|
||||
|
||||
void MGPipeClientClearVerbBoundary() {
|
||||
MGPipeStampAccess::SetServerStamped(MGPipeClientInputs(), false);
|
||||
}
|
||||
|
||||
namespace { Bool g_serverContextLive = false; }
|
||||
|
||||
void MGPipeServerSetContextLive(Bool live) {
|
||||
g_serverContextLive = live;
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
}
|
||||
|
||||
Bool MGPipeServerContextIsLive() { return g_serverContextLive; }
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace { const void* g_serverOwnedWindow = nullptr; }
|
||||
|
||||
void MGPipeServerSetOwnedWindow(const void* window) { g_serverOwnedWindow = window; }
|
||||
|
||||
const void* MGPipeServerOwnedWindow() { return g_serverOwnedWindow; }
|
||||
#endif
|
||||
|
||||
void MGPipeServerBlockNoteIdentity() {
|
||||
// D10: THE WIDER PREDICATE, and the narrow one was a latent crash rather than a missing
|
||||
// optimisation. Gated on the REHEARSAL, this early-returned for the whole life of a
|
||||
// spawn server - because that knob is off by default and nothing in the spawn shape
|
||||
// turns it on - so the server's gPipeInputs.ContextIdentity() stayed nullptr forever.
|
||||
// DirectGLES' fb-slot memo compares that identity FIRST and with no generation, so a
|
||||
// nullptr against its own nullptr initialiser reads as a CACHE HIT and hands out a slot
|
||||
// that was never filled. `st` lands the named Fatal at that site; this is the gate that
|
||||
// stops it being reachable in the first place.
|
||||
if (!MGPipeBlocksAreDistinct()) return;
|
||||
// The server cannot name the client's GLContext - under a real transport it is in
|
||||
// another process - so the identity the server block carries is the server's OWN
|
||||
// served-context clock: MGPipeApplierContextSerial moves exactly when the served
|
||||
// context does (MGPipeApplierReset), which is all the backends' per-context memo keys
|
||||
// ask of an identity. The odd-encoding keeps the token non-null at serial 0: a null
|
||||
// identity reads as a hit against DirectGLES' zero-initialised fb-slot memo cache and
|
||||
// hands out a null slot - an unnamed crash where the rehearsal exists to produce a
|
||||
// named one.
|
||||
const Uint64 serial = MGPipeApplierContextSerial();
|
||||
MGPipeStampAccess::SetIdentity(gPipeInputs, g_serverContextLive,
|
||||
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(
|
||||
(serial << 1) | Uint64{1})));
|
||||
}
|
||||
|
||||
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
// P5e (gl), ID-115: THE POSITIVE CONTROL'S ONLY HONEST SIGNAL, and it belongs HERE
|
||||
// because this is the line the whole strict mechanism hangs off. The poison, rsp and
|
||||
// the BARRIER-PULLED verdict are all reachable only after this stamp, and the monolith
|
||||
// arm never stamps at all - so "the strict lane is green" and "strict was never armed"
|
||||
// were observationally identical, which is how a lane whose seven passing entries
|
||||
// included no record-carrying split GL scenario read as rigour. Behind Enabled(), which
|
||||
// is the file's rule for counters (`rsp` beside it does the same) and keeps the stamp a
|
||||
// table lookup and a branch-free fill when stats are off.
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ServerVerbBoundaries, 1);
|
||||
}
|
||||
MGPipeFilledState& filled = MGPipeStampAccess::Filled(inputs);
|
||||
MGPipeStampAccess::SetVerb(inputs, verb);
|
||||
// Starts at 1 for MGPipeValidateForVerb's reason: FilledGen == 0 is "never filled" on
|
||||
// BOTH branches of MGPipeInputFieldIsFresh, so the zeroing below is a real withdrawal
|
||||
// rather than a stamp that happens to be old.
|
||||
++filled.CurrentVerbSerial;
|
||||
// STAMPED: in this verb's class AND answerable out of the records the applier has
|
||||
// already applied. WITHDRAWN (0): everything else - which is BARRIER-PULLED, FATAL,
|
||||
// and anything the verb's own may-read table says this verb does not read.
|
||||
//
|
||||
// The withdrawal is the load-bearing half of the rule: the client's residual fill
|
||||
// stamped all 63 fields at its own verb boundary, so without it every field would
|
||||
// read fresh on the server, `rsp` would be identically 0 and the exit gate would be
|
||||
// decoration. It also cancels the sticky exemption for free - generated/
|
||||
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so 0 wins over
|
||||
// kMGPipeInputFieldSticky without a line of the generated file changing.
|
||||
//
|
||||
// THE VERDICT IS THE SAME EXPRESSION; it is just precomputed per verb class
|
||||
// (AnswerableMaskForClass above) instead of re-derived 63 times per verb, so what is
|
||||
// left here is a table lookup and a branch-free fill.
|
||||
const MGPipeFieldMask& answerable = AnswerableMaskForVerb(verb);
|
||||
const Uint64 serial = filled.CurrentVerbSerial;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const Uint64 bit = (answerable.Words[i / 64] >> (i % 64)) & Uint64{1};
|
||||
filled.FilledGen[i] = serial & (Uint64{0} - bit);
|
||||
}
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// Negative control B's server half (ServerPoisonOmission above): the one withheld pair.
|
||||
{
|
||||
const ServerPoisonOmission& omission = ServerOmission();
|
||||
if (omission.Armed && omission.Verb == verb) {
|
||||
filled.FilledGen[static_cast<SizeT>(omission.Field)] = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
MGPipeStampAccess::SetServerStamped(inputs, true);
|
||||
// P5f (f1): the server block's identity rides the stamp (CONTRACT-P5E §3.2's
|
||||
// "SetIdentity moves to ApplyOne" - the stamp is the per-verb half of ApplyOne, and
|
||||
// refreshing here rather than once at Attach keeps the token in step with
|
||||
// MGPipeApplierContextSerial across a context switch). A no-op with the rehearsal off.
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
}
|
||||
|
||||
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
|
||||
|
||||
Uint64 MGPipeResidualPullCount() { return g_residualPulls; }
|
||||
void MGPipeResetResidualPullCountForTesting() { g_residualPulls = 0; }
|
||||
|
||||
void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped) {
|
||||
// OUTSIDE A SERVER-STAMPED VERB THIS IS THE MONOLITH ANSWER, UNCHANGED. A split BUILD
|
||||
// running MOBILEGL_TRANSPORT=monolith - every unit and integration-gpu lane of
|
||||
// build-split - has a client that stamped all 63 fields, so a stale read there is the
|
||||
// same defect it is in a verify build. Softening it on the build rather than on the
|
||||
// stamp would take 1842 unit cases' ability to go red away with it.
|
||||
//
|
||||
// AND A READ OUTSIDE THE VERB'S OWN CLASS IS STILL FATAL even for a BARRIER-PULLED
|
||||
// field: the value it would be answered with was never copied for this verb, so
|
||||
// counting it would trade a loud staleness for a quiet one.
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// An injected omission is reported as what it models - a stamp nobody wrote, the
|
||||
// client filler's own Fatal{UnmigratedPipeInput, "<Field>@<Verb>"} - and never as the
|
||||
// RoleViolation below, which would blame the wire for a withdrawal the control made.
|
||||
if (serverStamped && ServerOmitsStamp(verb, field)) MGPipeInputPoisonFatalForVerb(field, verb);
|
||||
#endif
|
||||
if (!serverStamped ||
|
||||
kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled ||
|
||||
!FieldIsInVerbClass(field, verb)) {
|
||||
// P5c (gt, CONTRACT-P5C §6 layer 1): under a server stamp, a stale read of a field
|
||||
// whose row is RECORD-SUPPLIED or APPLIER-DERIVED - i.e. a value a pushed record
|
||||
// DOES carry, read where this verb's stamp does not cover it - is a role violation
|
||||
// named by its surface, not a generic unmigrated read: the wire already owns the
|
||||
// answer, so reaching past it into the residual fill is rule E's shape. A
|
||||
// BARRIER-PULLED field outside the verb's class and a FATAL field keep the poison
|
||||
// Fatal - those are the stamp table's own verdicts, not a role's overreach.
|
||||
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[static_cast<SizeT>(field)];
|
||||
if (serverStamped && (ownership == MGPipeFieldOwnership::kRecordSupplied ||
|
||||
ownership == MGPipeFieldOwnership::kApplierDerived)) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"%s\"} - the server read this field stale "
|
||||
"at %s, but a pushed record carries it (the row is %s): the read reached "
|
||||
"the client's residual fill for a value the wire already owns",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb),
|
||||
MGPipeFieldOwnershipName(ownership));
|
||||
std::abort();
|
||||
}
|
||||
MGPipeInputPoisonFatalForVerb(field, verb);
|
||||
}
|
||||
CountBarrierPull(field, verb);
|
||||
}
|
||||
|
||||
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped) {
|
||||
if (!serverStamped) return false;
|
||||
const MGPipeFieldOwnership narrowed = MGPipeFieldOwnershipOf(field, arg0);
|
||||
if (narrowed == MGPipeFieldOwnershipOf(field)) return false; // the argument narrows nothing
|
||||
if (narrowed == MGPipeFieldOwnership::kFatal) {
|
||||
// The field's own stamp says fresh - the applier really did write the half that has
|
||||
// a carrier - so only the argument can say that THIS read is unserved. THE MESSAGE
|
||||
// NAMES THE ARGUMENT, because without it this line is byte-identical to what a
|
||||
// genuinely stale read of the OTHER half would print, and the whole case for
|
||||
// narrowing by argument rather than by a second field id is that the reader is told
|
||||
// which half they asked for.
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [argument 0 = %u is %s while the "
|
||||
"field is %s]",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb), arg0,
|
||||
MGPipeFieldOwnershipName(narrowed),
|
||||
MGPipeFieldOwnershipName(MGPipeFieldOwnershipOf(field)));
|
||||
std::abort();
|
||||
}
|
||||
if (narrowed == MGPipeFieldOwnership::kBarrierPulled) {
|
||||
CountBarrierPull(field, verb);
|
||||
return true; // decided here; the field-level check must not count it again
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MGPipeStickyForwardPull(MGPipeInputField field) {
|
||||
// The seven carry no MGP_INPUT_CHECK at all (the declared exception argued at
|
||||
// PipeInputs.h's F-class block), so freshness can never reach them and neither can the
|
||||
// stamp's withdrawal. This is the only thing that puts them in `rsp`.
|
||||
if (!gPipeInputs.ServerStampedVerb()) return;
|
||||
const auto ownership = MGPipeFieldOwnershipOf(field);
|
||||
if (ownership == MGPipeFieldOwnership::kFatal)
|
||||
MGPipeInputPoisonFatalForVerb(field, gPipeInputs.CurrentVerb());
|
||||
if (ownership == MGPipeFieldOwnership::kBarrierPulled)
|
||||
CountBarrierPull(field, gPipeInputs.CurrentVerb());
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/ServerRole.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The server role's bring-up, as one entry point two shapes share.
|
||||
//
|
||||
// Steps 1 and 2 of Init.cpp's split bring-up - the server's private
|
||||
// BackendObject, and the two CallMask halves computed from THAT backend's own
|
||||
// function table. Under `inproc` InitSplitRoles calls it and then starts the
|
||||
// client in the same process; under `spawn` the child's ServerMain calls it and
|
||||
// then accepts a connection instead.
|
||||
//
|
||||
// ONE ENTRY POINT, not two, because the capability bits are a statement about
|
||||
// the server's backend and only the server can compute them. A second copy in
|
||||
// ServerMain would be a second answer to "does this backend own the XFB
|
||||
// capture", diverging silently the first time a backend registers a slot.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
// DECLARED ONLY IN A DISAGGREGATED BUILD. G1 requires the pull build's symbol
|
||||
// set and .text to be byte-identical, and a definition guarded only on the
|
||||
// inside still emits a symbol - measured, not assumed.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
|
||||
// Creates the server's BackendObject and publishes the consumed-subsystem
|
||||
// mask and the capability bits onto ServerSessionInstance(). Does NOT create
|
||||
// an EGL context: that happens later, on the apply thread, when the client's
|
||||
// first eglMakeCurrent crosses as a blocking control request.
|
||||
//
|
||||
// Returns false when the backend could not be created, and the caller must
|
||||
// then REFUSE TO CONTINUE rather than fall back - a server process that came
|
||||
// up without a backend would accept a session it can never apply.
|
||||
Bool InitServerRoleForSpawn();
|
||||
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user