mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
189
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c39d2421 | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a | ||
|
|
6a80a82dd3 | ||
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 | ||
|
|
bce34d7fac | ||
|
|
f91857266f | ||
|
|
49cb1be0fd | ||
|
|
a51c68bb2c | ||
|
|
1c723a6cfc | ||
|
|
44805bfa07 | ||
|
|
257fcbfd0b | ||
|
|
2b46a3db96 | ||
|
|
0b36621069 | ||
|
|
9ee2e0a1db | ||
|
|
6b6623ae72 | ||
|
|
a6e029734b | ||
|
|
e7d6bfddac | ||
|
|
f2c879528f | ||
|
|
eaba4ac1dc | ||
|
|
ed6578954e | ||
|
|
e005c8b6cb | ||
|
|
535b5e3095 | ||
|
|
1b05a84928 | ||
|
|
7ccb762936 | ||
|
|
442e7eec1c | ||
|
|
2787d15706 | ||
|
|
74ce58a6c7 | ||
|
|
bb122ebd4f | ||
|
|
9b0ed5b3af | ||
|
|
0e31c1481b | ||
|
|
811f32760e | ||
|
|
01381a0404 | ||
|
|
0f02b0fdb1 | ||
|
|
3be02abf47 | ||
|
|
f91d6b676c | ||
|
|
1ebf191f94 | ||
|
|
ccad803023 | ||
|
|
21159caf31 | ||
|
|
ea4819a21d | ||
|
|
6b882b3ccf | ||
|
|
96bd36c50b | ||
|
|
46fbd837b3 | ||
|
|
796a57a115 | ||
|
|
62a2dae5ba | ||
|
|
532836c058 | ||
|
|
2fced2241b | ||
|
|
21b5fc2d92 | ||
|
|
1f753ab5fa | ||
|
|
3ed9501be5 | ||
|
|
7311251f30 | ||
|
|
7625cf450d | ||
|
|
450eb209b6 | ||
|
|
8c5c39b3c3 | ||
|
|
64a0ea397c | ||
|
|
205d837942 | ||
|
|
b5e9339c66 | ||
|
|
e310e3e9ff | ||
|
|
a0bf4a83bc | ||
|
|
97facf777b | ||
|
|
5cfbb716c0 | ||
|
|
64c3411d70 | ||
|
|
f3a0d9e0a3 | ||
|
|
068786e812 | ||
|
|
8e7cc62c24 | ||
|
|
d2a36d65a3 | ||
|
|
a020de76e3 | ||
|
|
574634adfa | ||
|
|
e71d715e1a | ||
|
|
7b946fd527 | ||
|
|
8f3ce5f5b7 | ||
|
|
21ec744ef2 | ||
|
|
faa7b17da3 | ||
|
|
f6849fc0b3 | ||
|
|
4d1d4f6225 | ||
|
|
cef81df73f | ||
|
|
c4e6ea1f23 | ||
|
|
b4e07ce651 | ||
|
|
ff324057ad | ||
|
|
ef4c6dbe0a | ||
|
|
19fc7346c5 | ||
|
|
bcb0e894ef | ||
|
|
794c10e56c | ||
|
|
28390667d7 | ||
|
|
90dd9bec77 | ||
|
|
7994ca31d3 | ||
|
|
5705e05156 | ||
|
|
ab62f81545 | ||
|
|
f6cf04d6d7 | ||
|
|
38eb9589f9 | ||
|
|
99ebf67a3d | ||
|
|
2292e99476 | ||
|
|
4c5afecc71 | ||
|
|
1c5744f2be | ||
|
|
22859b0958 | ||
|
|
7aa958fbc9 | ||
|
|
a4bd4e04a1 | ||
|
|
09459edb6b | ||
|
|
8af6ebc174 | ||
|
|
577cd8c670 | ||
|
|
5267243404 | ||
|
|
33c2715912 | ||
|
|
3068cdadf8 | ||
|
|
6dd0201bf2 | ||
|
|
05bef7118b | ||
|
|
94e75fef79 | ||
|
|
43bcd03dca | ||
|
|
2ce0595fab | ||
|
|
ba9af18033 | ||
|
|
f7d63f88fa | ||
|
|
6cf5a7744e | ||
|
|
ca3d24f5ea | ||
|
|
cc34d34706 | ||
|
|
757b31592d | ||
|
|
dbae4eda10 | ||
|
|
fa5ff5d168 | ||
|
|
994ae372f8 | ||
|
|
7ba012adf9 | ||
|
|
b1fdffd767 | ||
|
|
18c17ae5ca | ||
|
|
16c010985f | ||
|
|
6f64ec0f51 | ||
|
|
5d47698349 | ||
|
|
7b593e39ef | ||
|
|
d83b4dbbb5 | ||
|
|
964a7fcc92 | ||
|
|
5a7bd9942d | ||
|
|
21a43bf6a4 | ||
|
|
5b6dec2d81 | ||
|
|
543c29bf86 | ||
|
|
ef562ee9b5 | ||
|
|
fa0f6693d0 | ||
|
|
a6c362c6ce | ||
|
|
921504eccf | ||
|
|
7c5fc03b26 | ||
|
|
ed29e63543 | ||
|
|
5c8a9c41d6 | ||
|
|
efa0345c36 | ||
|
|
ce0f18969c | ||
|
|
7ce0966e7d | ||
|
|
1c6ca2753f | ||
|
|
61b0532865 | ||
|
|
f5b8a505ed | ||
|
|
8371365db5 | ||
|
|
b219992ee3 | ||
|
|
94233ef928 | ||
|
|
0827d7a539 | ||
|
|
5248b8b746 | ||
|
|
d868e1c476 | ||
|
|
c3412ca394 | ||
|
|
5ccaff37af | ||
|
|
71e29f9d58 | ||
|
|
8ad07c222c | ||
|
|
5722094d6f | ||
|
|
4831387cf0 | ||
|
|
d03b72267a | ||
|
|
847ec74f48 | ||
|
|
e02e5caa17 | ||
|
|
1958934594 | ||
|
|
dec0c5eaff | ||
|
|
b6a44cd1e2 | ||
|
|
85f45d0e44 | ||
|
|
404236d337 | ||
|
|
6ea948779e |
@@ -1,6 +1,10 @@
|
||||
#!/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
|
||||
@@ -62,57 +66,6 @@ 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"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/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'
|
||||
}
|
||||
+111
-8
@@ -11,6 +11,9 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
CCACHE_COMPRESS: "true"
|
||||
@@ -41,12 +44,11 @@ jobs:
|
||||
gradle-version: 8.10.2
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
@@ -125,6 +127,28 @@ 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)"
|
||||
@@ -201,9 +225,41 @@ 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')"
|
||||
@@ -379,6 +435,9 @@ jobs:
|
||||
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
|
||||
@@ -411,6 +470,24 @@ 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}" \
|
||||
@@ -450,6 +527,13 @@ 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
|
||||
if: always()
|
||||
@@ -531,22 +615,41 @@ jobs:
|
||||
)
|
||||
|
||||
if ((${#failed_cases[@]})); then
|
||||
echo "Retaining fixtures for failed retrace case(s):"
|
||||
echo "Retaining fixtures and results for failed retrace case(s):"
|
||||
printf ' %s\n' "${!failed_cases[@]}"
|
||||
else
|
||||
echo "All retrace jobs succeeded; no fixtures need to be retained."
|
||||
echo "All retrace jobs succeeded; nothing needs 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
|
||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||
((retained += 1))
|
||||
continue
|
||||
keep=1
|
||||
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})"
|
||||
|
||||
+197
-8
@@ -1,4 +1,4 @@
|
||||
name: Test
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,6 +11,9 @@ on:
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
BUILD_DIR: build-linux
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
@@ -34,12 +37,11 @@ jobs:
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||
|
||||
- name: Prepare Vulkan SDK
|
||||
@@ -83,6 +85,8 @@ jobs:
|
||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_TEST=ON \
|
||||
-DMOBILEGL_BUILD_BENCHMARK=ON \
|
||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
||||
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
|
||||
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
||||
-DBENCHMARK_ENABLE_TESTING=OFF \
|
||||
@@ -95,6 +99,28 @@ 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 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 }}-test-${{ github.job }}-ccache-v1
|
||||
run: gh cache delete "${CACHE_KEY}" || true
|
||||
|
||||
- name: Save ccache
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
|
||||
- name: Package Linux runtime
|
||||
run: |
|
||||
mkdir -p ci-artifacts
|
||||
@@ -110,6 +136,7 @@ jobs:
|
||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
||||
"${SHARED_LIBS[@]}"
|
||||
|
||||
- name: Upload Linux runtime
|
||||
@@ -159,12 +186,102 @@ jobs:
|
||||
- name: Test
|
||||
working-directory: build-linux
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L unit --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L unit --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-linux
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get CMake
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Install runtime dependencies
|
||||
# Same set as the benchmark job, for the same reason: the scenarios bring
|
||||
# up real headless EGL (llvmpipe) and Vulkan (lavapipe) contexts, and
|
||||
# libegl-mesa0 - the EGL vendor library behind glvnd's libegl1 dispatch -
|
||||
# only arrives as a Recommends.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||
|
||||
- name: Download Linux runtime
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mobilegl-linux-runtime
|
||||
path: .
|
||||
|
||||
- name: Unpack Linux runtime
|
||||
run: tar -xzf mobilegl-linux-runtime.tgz
|
||||
|
||||
- name: Normalize CTest command paths
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
|
||||
text = path.read_text()
|
||||
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
|
||||
path.write_text(text)
|
||||
PY
|
||||
|
||||
- name: Integration scenarios
|
||||
working-directory: build-linux
|
||||
# REQUIRE_GPU makes a driverless runner FAIL instead of skipping every
|
||||
# scenario - an all-skip run is otherwise indistinguishable from a pass,
|
||||
# which is how a five-month-old draw-dropping bug survived unseen until
|
||||
# this lane existed.
|
||||
#
|
||||
# The lavapipe ICD pin lives in the build-linux configure
|
||||
# (-DMOBILEGL_ITEST_VK_ICD), NOT here: the configure bakes it into each
|
||||
# test's ctest ENVIRONMENT property, and a property entry OVERRIDES the
|
||||
# job environment - a VK_ICD_FILENAMES exported here would be silently
|
||||
# ignored while looking like it works. This lane runs on lavapipe
|
||||
# deterministically, not on whichever of the eight Mesa ICDs a GPU-less
|
||||
# runner enumerates first.
|
||||
#
|
||||
# Cores are armed so that any crash - the harness pre-flight child's
|
||||
# included - leaves /tmp/core.*, which the failure-only step below ships
|
||||
# as an artifact. Analyzing a downloaded core against the runtime
|
||||
# artifact's binary in an ubuntu-24.04 userspace reproduces the exact
|
||||
# crash stack without burning a CI round on an in-workflow debugger.
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L integration-gpu --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
||||
fi
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: integration-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-linux
|
||||
@@ -208,7 +325,18 @@ jobs:
|
||||
|
||||
- name: Benchmark
|
||||
working-directory: build-linux
|
||||
run: ctest -V -C Release -L benchmark --no-tests=error
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
ctest -V -C Release -L benchmark --no-tests=error
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: benchmark-core-dumps
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
build-retrace:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -216,6 +344,10 @@ jobs:
|
||||
- build-linux
|
||||
- test
|
||||
- benchmark
|
||||
- integration
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
BUILD_DIR: build-retrace
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
@@ -240,12 +372,11 @@ jobs:
|
||||
uses: lukka/get-cmake@v4.3.3
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
|
||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||
|
||||
- name: Prepare Vulkan SDK
|
||||
@@ -311,6 +442,21 @@ jobs:
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
- name: Release superseded ccache entry
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
run: gh cache delete "${CACHE_KEY}" || true
|
||||
|
||||
- name: Save ccache
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
|
||||
|
||||
- name: Normalize CTest command paths
|
||||
run: |
|
||||
python - <<'PY'
|
||||
@@ -343,6 +489,7 @@ jobs:
|
||||
needs:
|
||||
- test
|
||||
- benchmark
|
||||
- integration
|
||||
outputs:
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
steps:
|
||||
@@ -366,9 +513,41 @@ 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')"
|
||||
@@ -456,6 +635,8 @@ jobs:
|
||||
- name: Retrace and validate
|
||||
working-directory: build-retrace/tools/trace_replay
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||
fi
|
||||
@@ -470,6 +651,14 @@ jobs:
|
||||
fi
|
||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: retrace-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
|
||||
path: /tmp/core.*
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload actual image
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
+3
-3
@@ -7,9 +7,6 @@
|
||||
[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
|
||||
@@ -34,3 +31,6 @@
|
||||
[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
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 900b29d449...6f12598784
@@ -20,6 +20,81 @@ set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to
|
||||
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)
|
||||
@@ -107,6 +182,7 @@ 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)
|
||||
@@ -201,13 +277,22 @@ set(SOURCE_FILES
|
||||
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/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.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/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.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/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -296,6 +381,7 @@ set(SOURCE_FILES
|
||||
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/ProgramSpirvTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||
// SpirvValidationEnabled).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
@@ -77,6 +82,14 @@ namespace MobileGL::MG_Config {
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// 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_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
@@ -84,6 +97,13 @@ namespace MobileGL::MG_Config {
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool AvoidExplicitLodBias = false;
|
||||
// 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
|
||||
@@ -97,6 +117,13 @@ namespace MobileGL::MG_Config {
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = 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;
|
||||
// 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
|
||||
@@ -136,6 +163,19 @@ namespace MobileGL::MG_Config {
|
||||
// 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;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -167,13 +167,17 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
@@ -183,6 +187,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
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");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
+37
-4
@@ -9,10 +9,20 @@
|
||||
#pragma once
|
||||
|
||||
// ============== Platform-specific definitions and macros ============== //
|
||||
#ifdef __ANDROID__
|
||||
#undef __ANDROID_API__
|
||||
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
|
||||
#endif
|
||||
// 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 _WIN32
|
||||
#ifndef NOMINMAX
|
||||
@@ -37,6 +47,23 @@
|
||||
#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
|
||||
@@ -68,6 +95,12 @@
|
||||
#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 FastSTL
|
||||
#include <FastSTL/UnorderedMap.h>
|
||||
// Include ska::flat_hash_map
|
||||
#include <ska/flat_hash_map.hpp>
|
||||
|
||||
// Include xxHash
|
||||
#include <xxhash.h>
|
||||
|
||||
+7
-5
@@ -14,6 +14,7 @@
|
||||
#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/Query/GL_Query.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
@@ -45,12 +46,13 @@ namespace MobileGL {
|
||||
// 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).
|
||||
// GL syncs and queries die with their contexts, and every context is gone
|
||||
// by the time full teardown runs: drain both live registries 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 / DeleteBackendQuery).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
|
||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||
// reconcile every runtime-gated advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -924,7 +926,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, 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,
|
||||
@@ -940,10 +944,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// picks a whole different shader for draw_buffers without
|
||||
// explicit_attrib_location. DirectVulkan advertises both.
|
||||
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
|
||||
// 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. The host ES driver
|
||||
// has had the same texture parameter since ES 3.1, which every device MobileGL
|
||||
// runs on provides.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// 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};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
if (drawIndirectSupported) {
|
||||
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||
}
|
||||
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||
// that incomplete case.
|
||||
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||
@@ -960,6 +988,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// Only advertised when the device driver actually has usable timer queries
|
||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -1002,6 +1039,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
||||
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
||||
funcsTable.GL.DrawRangeElements = DrawRangeElements;
|
||||
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
||||
@@ -1151,6 +1189,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
|
||||
// which). Reporting 0 instead was considered and rejected: MobileGL advertises an OpenGL
|
||||
// 4.x context, where buffer textures are core and the limit has a spec minimum of 65536,
|
||||
// so 0 is not a legal answer and applications are not written to survive it. GL offers no
|
||||
// way to say "this core feature is missing", so the honesty is carried outside the limit:
|
||||
// FillInGLESCapabilities logs the tier, glTexBuffer and the program build each name the
|
||||
// missing capability at MGLOG_I, and the driver POST carries a "Buffer textures" row that
|
||||
// FAILs on this tier.
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
||||
|
||||
@@ -67,9 +67,12 @@ 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 are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,8 @@ 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);
|
||||
@@ -117,6 +119,24 @@ 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool InProcessTeardown();
|
||||
void EnsureProcessTeardownSentinel();
|
||||
|
||||
// Generation of the backend ES context that owns the driver ids currently handed
|
||||
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
||||
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
|
||||
// construction and compares it in its destructor: a twin outliving its context
|
||||
// must NOT glDelete* its id, because a successor context may already have recycled
|
||||
// that name and the delete would take out a live object of the new context.
|
||||
extern Uint g_backendContextGeneration;
|
||||
|
||||
// Which optional pieces of state a draw needs synchronized before it is issued.
|
||||
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||
// draws that provably cannot read them.
|
||||
@@ -74,14 +82,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
|
||||
// no-ops when the program does not read the corresponding builtin.
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
void SetCurrentDrawID(Uint32 drawId);
|
||||
// GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every
|
||||
// command that has none - including all the DrawArrays forms - so every draw path that
|
||||
// does not carry one must leave this at zero rather than inherit the last draw's value.
|
||||
void SetCurrentBaseVertex(Int32 baseVertex);
|
||||
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
||||
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||
// batch and would therefore be wrong.
|
||||
Bool CurrentProgramReadsDrawID();
|
||||
// Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw
|
||||
// its own base vertex through a uniform either.
|
||||
Bool CurrentProgramReadsBaseVertex();
|
||||
// Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw
|
||||
// has synced the program - where "does not read it" is indistinguishable from "cannot be
|
||||
// asked yet". Answers true whenever the backend twin is missing or predates the current
|
||||
// link.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||
|
||||
template <typename StateObject, typename BackendObject>
|
||||
class StateBackendObjectRegistry {
|
||||
@@ -121,6 +141,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Null when no live state object owns this key. The result points into the map, so
|
||||
// it stays valid only until the next GetOrCreate/Find/CollectGarbage on this registry.
|
||||
// Take that literally, including for Find: the map is open-addressed and erases by
|
||||
// shifting the rest of the probe cluster into the hole, so an erase relocates entries
|
||||
// OTHER than the erased one - and Find erases, whenever it lands on a key whose state
|
||||
// object has expired. Callers that need the twin across another registry call must copy
|
||||
// the BackendPtr out (or keep only the pointee, which is heap-allocated and never moves).
|
||||
BackendPtr* Find(StateObject* stateObj) {
|
||||
const auto entryIt = m_entries.find(stateObj);
|
||||
if (entryIt == m_entries.end()) {
|
||||
@@ -261,6 +286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// context loss.
|
||||
Bool persistentMapped = false;
|
||||
void* persistentPtr = nullptr;
|
||||
// The GL store behind `id` was created with glBufferStorageEXT and is
|
||||
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
|
||||
// recycled through the size-keyed buffer pool. Tracked separately from
|
||||
// persistentMapped because the two come apart: a glMapBufferRange that fails
|
||||
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
|
||||
// no map, and a respecification then has to retire the id rather than hand it
|
||||
// to glBufferData, which the driver would silently refuse.
|
||||
Bool immutableStorage = false;
|
||||
};
|
||||
|
||||
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||
@@ -373,6 +406,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SyncClientSideAttributesForDrawArrays(
|
||||
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
|
||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||
void Bind() const;
|
||||
|
||||
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||
@@ -429,6 +463,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||
PendingAttribValueMask m_pendingAttribValueMask;
|
||||
Uint m_backendVAOId = 0;
|
||||
// ES context generation the VAO id and client-attribute buffer ids were
|
||||
// created under; ids from a dead context must never be deleted against a
|
||||
// successor context (both contexts restart GL names at 1).
|
||||
Uint m_contextGeneration = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
@@ -441,6 +479,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 m_syncedConfigVersion = 0;
|
||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||
m_syncedAttributeVersions;
|
||||
// Byte shift currently baked into the instanced arrays' offsets by the baseInstance
|
||||
// emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it
|
||||
// is deliberately NOT covered by the config version: the frontend never bumps for it.
|
||||
// Kept here because it describes what was last EMITTED, which is what the next sync
|
||||
// has to correct.
|
||||
Uint32 m_syncedFetchBaseInstance = 0;
|
||||
};
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||
@@ -454,6 +498,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void InvalidateVAOBindingCache();
|
||||
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||
void NoteVAOIdDeleted(Uint id);
|
||||
|
||||
// baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an
|
||||
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
|
||||
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
|
||||
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
|
||||
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
|
||||
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
|
||||
void SetPendingFetchBaseInstance(Uint32 baseInstance);
|
||||
Uint32 GetPendingFetchBaseInstance();
|
||||
|
||||
class ScopedFetchBaseInstance {
|
||||
public:
|
||||
explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); }
|
||||
~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); }
|
||||
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
|
||||
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
|
||||
};
|
||||
} // namespace VertexArrayImpl
|
||||
|
||||
namespace TextureImpl {
|
||||
@@ -613,6 +674,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
// Latches the "this driver has no buffer textures" report to once per texture. The
|
||||
// report is emitted from the respecify path, which bails before recording the state
|
||||
// it was asked to apply - so without the latch the texture stays permanently dirty
|
||||
// and every draw of every frame logs the same line.
|
||||
Bool m_bufferTextureUnsupportedReported = false;
|
||||
StateTextureBasicInfo m_prevTextureInfo;
|
||||
// Frontend content version at the last completed mipmap sync. The per-draw
|
||||
// clean probe compares this before rebuilding shape info and scanning
|
||||
@@ -638,8 +704,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||
// texture that never asks for the stencil aspect never emits the call. The
|
||||
// depth/stencil readback and replicate-blit emulations also write this parameter
|
||||
// raw, but only ever on their own scratch textures (never on an application
|
||||
// texture), so they cannot desynchronise this cache.
|
||||
GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
||||
Uint16 m_syncedSamplerVersion = 0;
|
||||
Uint16 m_syncedTextureParamsVersion = 0;
|
||||
// Set when the driver texture underneath was regenerated and has therefore lost every
|
||||
// parameter already pushed onto it: the params-version early-out has to be overridden
|
||||
// once, or an unchanged version would skip the re-push forever.
|
||||
Bool m_forceTextureParamsResync = false;
|
||||
// Same latch for the built-in sampler parameters.
|
||||
Bool m_forceSamplerResync = false;
|
||||
};
|
||||
|
||||
void ActivateTextureUnit(Uint unit);
|
||||
@@ -657,15 +735,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
extern Uint g_activeTextureUnit;
|
||||
// Bumped when the backend ES context is destroyed; texture ids stamped with
|
||||
// an older generation belong to a dead context and must not be deleted.
|
||||
extern Uint g_textureContextGeneration;
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {
|
||||
class BackendFramebufferObject {
|
||||
public:
|
||||
BackendFramebufferObject();
|
||||
// Deletes the driver framebuffer and scrubs the binding shadow. Without it every
|
||||
// frontend glDeleteFramebuffers leaked one ES framebuffer for the process lifetime;
|
||||
// an app that creates a framebuffer per readback (GL CTS packed_pixels does ~3300
|
||||
// per case) walked the driver into hundreds of megabytes of dead framebuffers and
|
||||
// out of the resources a later attachment needs.
|
||||
~BackendFramebufferObject();
|
||||
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
|
||||
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||
FramebufferTarget asTarget);
|
||||
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||
@@ -680,6 +763,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
private:
|
||||
Uint m_backendFBOId = 0;
|
||||
Uint m_contextGeneration = 0;
|
||||
|
||||
/* this will save buffers in its original form,
|
||||
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
||||
@@ -821,6 +905,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||
void InvalidateFramebufferBindingCache();
|
||||
// A driver framebuffer id is about to be deleted: ES reverts every target that
|
||||
// currently binds it to 0, so the binding shadow has to follow or the next
|
||||
// BindFramebufferId(0) would be deduped away and leave the deleted name bound.
|
||||
void NoteFramebufferIdDeleted(Uint id);
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||
@@ -997,11 +1085,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
void SetBaseVertex(Int32 baseVertex) const;
|
||||
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
||||
// actually reaches a shader read rather than being discarded.
|
||||
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||
// uniform write, and only such a program needs the reset after one.
|
||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
Uint GetContextGeneration() const { return m_contextGeneration; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
// shader failed to transpile or compile, or the link itself failed). Use()
|
||||
// must not leave the previously bound program current in that case.
|
||||
@@ -1010,6 +1103,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||
// Signature of the glShaderStorageBlockBinding override set the generated ESSL was
|
||||
// transpiled against (ES can only express a storage-block binding as the declared
|
||||
// qualifier, so the overrides are baked into the source). A mismatch means the
|
||||
// program is stale exactly like the clamp masks above.
|
||||
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1025,14 +1123,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Frontend link version this backend program (and its resource caches) was
|
||||
// built from; a mismatch means every link-derived cache here is stale.
|
||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
||||
// Image-uniform unit generation this backend program was GENERATED against.
|
||||
// Separate from the link version because it is not link state: ES forbids
|
||||
// glUniform1i on an image uniform, so RebindImageUniformsToFrontendUnits bakes the
|
||||
// unit into the ESSL, and a program built before glUniform1i moved that unit is as
|
||||
// stale as one built before a relink - while the sampler half, which really is
|
||||
// re-issued per draw, needs nothing of the sort.
|
||||
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
||||
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||
// resolve to are still the ones its ESSL was generated against.
|
||||
//
|
||||
// A fourth condition of the same family as the three above, and the only one that
|
||||
// reads live state rather than a program-side counter, because that is where the
|
||||
// dependency actually is. GLSL ES requires a format layout qualifier on every image
|
||||
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
|
||||
// qualifier is whatever glBindImageTexture named - so a declaration with no format
|
||||
// is compiled against the BINDING, and a rebind to a different format makes the
|
||||
// built program wrong. Keyed on the units the program's own images address (cached
|
||||
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
|
||||
// version above and forces a re-sync anyway), so the cost on a program with no
|
||||
// format-less image - which is all but a handful - is one empty-vector test.
|
||||
//
|
||||
// Deliberately NOT reached from glBindImageTexture: that entry point must never
|
||||
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
|
||||
// state and this comparison notices at the next Prepare, which is also what makes
|
||||
// an image first bound AFTER link work.
|
||||
Bool ImageUnitFormatsStillMatch() const;
|
||||
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
|
||||
// image-unit state. 0 when the program has no format-less image uniform.
|
||||
Uint64 ComputeImageUnitFormatSignature() const;
|
||||
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
Uint m_backendProgramId = 0;
|
||||
// ES context generation the backend program and its global UBO were created
|
||||
// under. A stale twin must be recreated, never deleted against a successor
|
||||
// context (both contexts restart GL names at 1).
|
||||
Uint m_contextGeneration = 0;
|
||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||
// knows it by.
|
||||
Uint m_frontendProgramId = 0;
|
||||
Uint m_backendGlobalUBOId = 0;
|
||||
Int m_baseInstanceUniformLocation = -1;
|
||||
Int m_drawIdUniformLocation = -1;
|
||||
Int m_baseVertexUniformLocation = -1;
|
||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
@@ -1040,6 +1176,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||
Uint m_fragColorBroadcastCount = 1;
|
||||
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
@@ -1050,6 +1188,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||
Uint32 m_syncedLinkVersion = ~0u;
|
||||
Uint32 m_syncedImageUnitVersion = ~0u;
|
||||
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||
// that declares a format on all of its images, which is the overwhelming majority -
|
||||
// and what keeps the per-draw comparison free for them.
|
||||
Vector<Int> m_formatlessImageUnits;
|
||||
Uint64 m_imageUnitFormatSignature = 0;
|
||||
SamplerPassMemo m_samplerPassMemo;
|
||||
};
|
||||
|
||||
@@ -1063,6 +1208,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||
// ES context is recreated.
|
||||
extern Uint g_lastUsedBackendProgramId;
|
||||
// Deletes `bufferId` only while it still belongs to the live ES context. Stale
|
||||
// generations are abandoned without a GL call: the old context already reclaimed
|
||||
// the buffer, and its numeric id may now name a live buffer in a successor context.
|
||||
void DeleteBackendProgramGlobalUbo(Uint& bufferId, Uint contextGeneration);
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||
g_backendProgramObjects;
|
||||
|
||||
@@ -1073,26 +1222,79 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// on the backend program (eliminated as unused, or the driver lacks the entry
|
||||
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||
// record either way.
|
||||
//
|
||||
// NOT how a rebinding reaches the shader. glShaderStorageBlockBinding has no ES
|
||||
// equivalent and is absent from every real ES driver, so this is a no-op there;
|
||||
// SyncToBackend bakes the effective binding into the ESSL it generates instead
|
||||
// (SpvcSession::SetShaderStorageBlockBinding). This is kept as the cheaper path on
|
||||
// a driver that does happen to expose the entry point.
|
||||
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||
// program that was just built. The frontend record is authoritative (only the
|
||||
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
||||
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
||||
// reseed-on-rebuild in BuildProgramResourceCache.
|
||||
// program that was just built - best effort, on the same "only where the driver has
|
||||
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
// Order-independent digest of the program's glShaderStorageBlockBinding overrides.
|
||||
// The generated ESSL carries them (ES has no way to move a storage block's binding
|
||||
// after link), so a program built against a different set is stale and the draw path
|
||||
// has to rebuild it. Computed from the values, so re-setting a block to the binding it
|
||||
// already has costs nothing. 0 when nothing was ever rebound.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
|
||||
// Everything the image-format bake needs from one walk of a program's uniform
|
||||
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
||||
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
||||
// on it.
|
||||
struct ImageFormatBakeInputs {
|
||||
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
||||
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
||||
// one is authoritative and is never overridden.
|
||||
UnorderedMap<String, Uint> glFormatByUniformName;
|
||||
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
||||
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
||||
// write into the emitted declaration instead. Disjoint from the map above by
|
||||
// construction: a format is baked into the module or completed in the text, never
|
||||
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
||||
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
||||
// Units those uniforms address, kept so the draw path can re-read their formats
|
||||
// without walking the reflection again.
|
||||
Vector<Int> units;
|
||||
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
||||
// image uniform, which is all but a handful.
|
||||
Uint64 signature = 0;
|
||||
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
||||
// declaration carries one qualifier, so there is nothing correct to bake and they
|
||||
// are dropped from the map above. Kept for diagnostics.
|
||||
Vector<String> conflictedNames;
|
||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||
Bool needsExtendedImageFormats = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
class BackendSamplerObject {
|
||||
public:
|
||||
BackendSamplerObject();
|
||||
// Deletes the driver sampler and clears the units whose binding shadow still names
|
||||
// this twin (a recycled heap address would otherwise false-skip a later Bind).
|
||||
// Frontend glDeleteSamplers used to leak the backend id for the process lifetime.
|
||||
~BackendSamplerObject();
|
||||
BackendSamplerObject(const BackendSamplerObject&) = delete;
|
||||
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
||||
void Bind(Uint unit);
|
||||
Uint GetBackendSamplerId() const;
|
||||
|
||||
private:
|
||||
Uint m_backendSamplerId = 0;
|
||||
Uint m_contextGeneration = 0;
|
||||
Bool m_isInitialized = false;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
Uint16 m_syncedSamplerVersion = 0;
|
||||
@@ -1110,12 +1312,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
class BackendRenderbufferObject {
|
||||
public:
|
||||
BackendRenderbufferObject();
|
||||
// Deletes the driver renderbuffer; frontend glDeleteRenderbuffers used to leak it
|
||||
// (with its whole image allocation) for the process lifetime.
|
||||
~BackendRenderbufferObject();
|
||||
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
|
||||
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
||||
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
||||
void Bind() const;
|
||||
|
||||
private:
|
||||
Uint m_backendRBOId = 0;
|
||||
Uint m_contextGeneration = 0;
|
||||
Bool m_isInitialized = false;
|
||||
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
||||
Int m_cacheWidth = 0;
|
||||
|
||||
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
g_resolvedTier =
|
||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||
&g_tierResolution);
|
||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
}
|
||||
|
||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||
@@ -267,24 +267,29 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||
if (g_announcedTiers & bit) return;
|
||||
g_announcedTiers |= bit;
|
||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
}
|
||||
|
||||
// The tier this particular batch can actually take. A tier is demoted here when
|
||||
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
// own index (the spec's value); nothing else observes the difference.
|
||||
// own index (the spec's value); nothing else observes the difference. The
|
||||
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
|
||||
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
|
||||
// which hands the driver the whole basevertex array, can only leave ONE value
|
||||
// in the uniform the shader reads.
|
||||
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||
tier == GLESMultiDrawMode::Compute;
|
||||
if (batched && programReadsDrawID) {
|
||||
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
|
||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||
: GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
@@ -371,7 +376,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
@@ -413,10 +419,12 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
}
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||
@@ -428,15 +436,17 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||
return true;
|
||||
}
|
||||
@@ -446,7 +456,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
|
||||
@@ -479,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
i);
|
||||
return false;
|
||||
@@ -500,11 +511,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
// The base vertex is folded into the rewritten index stream here, so the
|
||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||
// application passed for this sub-draw.
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
cursor += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||
return true;
|
||||
@@ -580,7 +596,7 @@ void main() {
|
||||
|
||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
return false;
|
||||
}
|
||||
const char* source = kFlattenComputeSource;
|
||||
@@ -591,14 +607,14 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||
if (program == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
@@ -609,7 +625,7 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
@@ -619,7 +635,7 @@ void main() {
|
||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||
g_computeProgramFailed = false;
|
||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -837,8 +853,15 @@ void main() {
|
||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||
// span is not legal at all. On success it hands back a flattened index stream.
|
||||
// A batch whose sub-draws carry their own base vertices cannot be flattened either
|
||||
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
|
||||
// Asked conservatively because this decision precedes PrepareForDraw - see
|
||||
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
|
||||
// once the batch is one draw the values are gone, whereas declining to flatten only
|
||||
// costs the unrolled tier.
|
||||
FlattenedStream flattened;
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
|
||||
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
|
||||
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||
}
|
||||
|
||||
@@ -852,8 +875,11 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that PrepareForDraw has synced the program, both questions have real answers;
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -861,16 +887,19 @@ void main() {
|
||||
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::MultiIndirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::Indirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::BaseVertex:
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::DrawElements:
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::Compute:
|
||||
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||
@@ -883,10 +912,15 @@ void main() {
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) {
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
drawcount, mode, type);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
@@ -416,10 +417,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
result = std::regex_replace(result, pattern, "$1flat $2");
|
||||
};
|
||||
|
||||
// Every stage that has an integer interface at all, on BOTH sides. Interpolation is
|
||||
// only ever consumed at a fragment input, so the qualifier is semantically inert on
|
||||
// a tessellation or geometry interface - but an ES linker still compares the two
|
||||
// sides of every interface and rejects a program whose producer says `flat` and
|
||||
// whose consumer does not. Covering only the stages that "need" it left exactly two
|
||||
// holes, and a program that used tessellation fell into both:
|
||||
// vertex `flat out uint` -> tess-control `in uint` (producer flat, consumer not)
|
||||
// tess-eval `out uint` -> geometry `flat in uint` (consumer flat, producer not)
|
||||
// Adreno answers "output ... interpolation mismatch with other stage" and the whole
|
||||
// program fails to link, which is a draw that silently paints nothing.
|
||||
//
|
||||
// Adding rather than stripping, because a fragment input's `flat` is load-bearing
|
||||
// (ESSL forbids an interpolated integer) and would have to be put back for the last
|
||||
// stage before the fragment shader anyway - so "everything integer is flat" is the
|
||||
// one rule that is consistent no matter which stages a program happens to have.
|
||||
switch (shaderType) {
|
||||
case GL_VERTEX_SHADER:
|
||||
addFlatQualifier("out");
|
||||
break;
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
case GL_GEOMETRY_SHADER:
|
||||
addFlatQualifier("in");
|
||||
addFlatQualifier("out");
|
||||
@@ -434,6 +452,175 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
String RetargetTextureBufferExtension(String glslCode,
|
||||
MG_External::GLESCapabilities::TextureBufferTier tier) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// SPIRV-Cross hardcodes the EXT spelling: CompilerGLSL::type_to_glsl emits
|
||||
// require_extension_internal("GL_EXT_texture_buffer") for any Dim=Buffer image
|
||||
// whenever it targets ESSL below 320, with no OES alternative and no way to
|
||||
// configure it. GL_OES_texture_buffer is functionally identical but is a separate
|
||||
// directive, and `#extension <name> : require` on a name the driver does not
|
||||
// advertise is a hard compile error - so on an OES-only driver the emitted shader
|
||||
// fails to compile for the sake of one token.
|
||||
//
|
||||
// Line comments are excluded by the directive check below; a `#extension` line inside
|
||||
// a /* */ block is not, and would be rewritten. That is harmless (it stays a comment)
|
||||
// and is not worth a preprocessor-aware scan here.
|
||||
//
|
||||
// Deliberately a directive rewrite and nothing more. The alternative - teaching the
|
||||
// SPIR-V to stop asking for the extension - is not available: the requirement is
|
||||
// synthesized by SPIRV-Cross from the image type itself, not carried in the module,
|
||||
// so there is nothing upstream to strip. Everything about the shader body that
|
||||
// actually uses the buffer texture is identical between the two extensions.
|
||||
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
|
||||
if (tier != Tier::ExtensionOES) {
|
||||
return glslCode;
|
||||
}
|
||||
static constexpr const char* kExtName = "GL_EXT_texture_buffer";
|
||||
static constexpr const char* kOesName = "GL_OES_texture_buffer";
|
||||
constexpr SizeT kExtNameLength = 21; // strlen("GL_EXT_texture_buffer")
|
||||
static_assert(sizeof("GL_EXT_texture_buffer") - 1 == kExtNameLength, "name length drifted");
|
||||
static_assert(sizeof("GL_OES_texture_buffer") - 1 == kExtNameLength,
|
||||
"the two spellings must be the same length for the in-place replace");
|
||||
|
||||
// Only rewrite the name where it is the whole subject of an #extension directive.
|
||||
// Two separate guards, both load-bearing:
|
||||
// * the directive check, so a line-comment mentioning the name is left alone;
|
||||
// * the identifier-boundary check, because GL_EXT_texture_buffer is a PREFIX of
|
||||
// GL_EXT_texture_buffer_object - a different, real extension that SPIRV-Cross
|
||||
// emits from the same `case DimBuffer:` on its legacy-desktop branch. Without
|
||||
// the boundary this pass would silently rewrite a request for that extension
|
||||
// into a request for a GL_OES_texture_buffer_object that does not exist.
|
||||
const auto isIdentifierChar = [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
||||
};
|
||||
SizeT searchFrom = 0;
|
||||
while (true) {
|
||||
const SizeT hit = glslCode.find(kExtName, searchFrom);
|
||||
if (hit == String::npos) {
|
||||
break;
|
||||
}
|
||||
searchFrom = hit + kExtNameLength;
|
||||
|
||||
// Identifier boundary on both sides, so the name is not a fragment of a longer one.
|
||||
if (hit > 0 && isIdentifierChar(glslCode[hit - 1])) {
|
||||
continue;
|
||||
}
|
||||
if (hit + kExtNameLength < glslCode.size() && isIdentifierChar(glslCode[hit + kExtNameLength])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Walk back to the start of the line and require that it is an #extension
|
||||
// directive, allowing whitespace between '#' and the keyword.
|
||||
SizeT lineStart = glslCode.rfind('\n', hit);
|
||||
lineStart = (lineStart == String::npos) ? 0 : lineStart + 1;
|
||||
SizeT cursor = lineStart;
|
||||
while (cursor < hit && std::isspace(static_cast<unsigned char>(glslCode[cursor]))) {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= hit || glslCode[cursor] != '#') {
|
||||
continue;
|
||||
}
|
||||
++cursor;
|
||||
while (cursor < hit && std::isspace(static_cast<unsigned char>(glslCode[cursor]))) {
|
||||
++cursor;
|
||||
}
|
||||
if (glslCode.compare(cursor, 9, "extension") != 0) {
|
||||
continue;
|
||||
}
|
||||
glslCode.replace(hit, kExtNameLength, kOesName);
|
||||
}
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
|
||||
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
|
||||
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
|
||||
// a format its core language does not know. GL_NV_image_formats is the only thing
|
||||
// that adds them, and it has to be requested by name.
|
||||
//
|
||||
// The caller decides `needed`: it knows which formats are in play (from the uniform
|
||||
// reflection and the image-unit bindings) and whether the driver advertises the
|
||||
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
|
||||
// this must never be emitted speculatively.
|
||||
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
|
||||
static constexpr const char* kExtName = "GL_NV_image_formats";
|
||||
if (!needed || glslCode.find(kExtName) != String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// After the #version line, which must stay first. Everything else about the header is
|
||||
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
|
||||
// directive still finds whichever one that is.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return kDirective + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + kDirective;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, kDirective);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BakeImageFormatQualifiers(String glslCode,
|
||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
|
||||
// reason: one line, one image uniform, the name in group 3.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
|
||||
// Every image format spelling GLSL has, so a declaration that already carries one is
|
||||
// recognised whatever it says - the caller's map is consulted only for declarations
|
||||
// with NO format, never to override a written one.
|
||||
static const std::regex existingFormatRegex(
|
||||
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
const SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(line, match, imageDeclRegex)) {
|
||||
const String name = match[3].str();
|
||||
const auto formatIt = esslFormatByUniformName.find(name);
|
||||
const String layoutContents = match[2].matched ? match[2].str() : String();
|
||||
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
|
||||
!std::regex_search(layoutContents, existingFormatRegex)) {
|
||||
if (match[1].matched) {
|
||||
const SizeT layoutOpen = line.find('(', match.position(1));
|
||||
line.insert(layoutOpen + 1, formatIt->second + ", ");
|
||||
} else {
|
||||
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result += line;
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
result += '\n';
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveLayoutBinding(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -470,6 +657,352 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsImagePassIdentifierChar(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
// Occurrences of `identifier` in `code` that are whole identifiers, i.e. not the
|
||||
// tail or head of a longer one. "goku" must not find "goku_hd" or "my_goku".
|
||||
SizeT CountIdentifierOccurrences(const String& code, const String& identifier) {
|
||||
if (identifier.empty()) return 0;
|
||||
SizeT count = 0;
|
||||
for (SizeT pos = code.find(identifier); pos != String::npos;
|
||||
pos = code.find(identifier, pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(code[pos - 1])) continue;
|
||||
const SizeT after = pos + identifier.size();
|
||||
if (after < code.size() && IsImagePassIdentifierChar(code[after])) continue;
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Bool ContainsIdentifier(const String& code, const String& identifier) {
|
||||
return CountIdentifierOccurrences(code, identifier) > 0;
|
||||
}
|
||||
|
||||
// The image format layout qualifiers ESSL accepts (GLSL ES 3.20 4.4.7 table 4.6 -
|
||||
// the ES-legal subset of what SPIRV-Cross's format_to_glsl can print). The
|
||||
// readonly/writeonly rule only applies to a declaration that carries one of them.
|
||||
Bool IsImageFormatQualifier(const String& token) {
|
||||
static constexpr StringView FORMATS[] = {
|
||||
"rgba32f", "rgba16f", "rg32f", "rg16f", "r11f_g11f_b10f",
|
||||
"r32f", "r16f", "rgba16", "rgb10_a2", "rgba8",
|
||||
"rg16", "rg8", "r16", "r8", "rgba16_snorm",
|
||||
"rgba8_snorm", "rg16_snorm", "rg8_snorm", "r16_snorm", "r8_snorm",
|
||||
"rgba32i", "rgba16i", "rgba8i", "rg32i", "rg16i",
|
||||
"rg8i", "r32i", "r16i", "r8i", "rgba32ui",
|
||||
"rgba16ui", "rgb10_a2ui", "rgba8ui", "rg32ui", "rg16ui",
|
||||
"rg8ui", "r32ui", "r16ui", "r8ui",
|
||||
};
|
||||
for (const StringView format : FORMATS) {
|
||||
if (token == format) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// "Except for image variables qualified with the format qualifiers r32f, r32i, and
|
||||
// r32ui, image variables must specify either memory qualifier readonly or the
|
||||
// memory qualifier writeonly." (GLSL ES 3.20 4.10)
|
||||
Bool IsMemoryQualifierExemptImageFormat(const String& token) {
|
||||
return token == "r32f" || token == "r32i" || token == "r32ui";
|
||||
}
|
||||
|
||||
// Comma-separated contents of a layout(...) list, each entry trimmed.
|
||||
Vector<String> SplitLayoutQualifierList(const String& layout) {
|
||||
Vector<String> tokens;
|
||||
SizeT start = 0;
|
||||
while (start <= layout.size()) {
|
||||
SizeT comma = layout.find(',', start);
|
||||
const Bool last = comma == String::npos;
|
||||
String token = layout.substr(start, last ? String::npos : comma - start);
|
||||
const SizeT first = token.find_first_not_of(" \t\r\n");
|
||||
if (first == String::npos) {
|
||||
token.clear();
|
||||
} else {
|
||||
token = token.substr(first, token.find_last_not_of(" \t\r\n") - first + 1);
|
||||
}
|
||||
if (!token.empty()) tokens.push_back(Move(token));
|
||||
if (last) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Trims both ends and collapses every internal whitespace run to one space, so a
|
||||
// qualifier list or array suffix can be spliced back into a rebuilt declaration
|
||||
// whatever the original spacing was.
|
||||
String NormalizeDeclarationSpacing(const String& text) {
|
||||
String out;
|
||||
out.reserve(text.size());
|
||||
Bool pendingSpace = false;
|
||||
for (const char c : text) {
|
||||
if (std::isspace(static_cast<unsigned char>(c))) {
|
||||
pendingSpace = !out.empty();
|
||||
continue;
|
||||
}
|
||||
if (pendingSpace) out += ' ';
|
||||
pendingSpace = false;
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// How an image builtin touches the image it is handed.
|
||||
enum class ImageBuiltinAccess { None, Load, Store, Unknown };
|
||||
|
||||
ImageBuiltinAccess ClassifyImageBuiltin(const String& name) {
|
||||
if (name == "imageStore") return ImageBuiltinAccess::Store;
|
||||
if (name == "imageLoad") return ImageBuiltinAccess::Load;
|
||||
// imageAtomic* both reads and writes, but ES only defines the atomics on
|
||||
// r32i/r32ui/r32f images - exactly the formats the rule above exempts - so this
|
||||
// pass has already skipped any declaration they can legally appear on. Load is
|
||||
// enough to keep the classification total without ever being acted upon.
|
||||
if (name.compare(0, 11, "imageAtomic") == 0) return ImageBuiltinAccess::Load;
|
||||
if (name == "imageSize" || name == "imageSamples") return ImageBuiltinAccess::None;
|
||||
// Some other identifier that starts with "image" and is being called: not a
|
||||
// shape this pass can reason about, so it poisons the declaration instead of
|
||||
// being guessed at.
|
||||
return ImageBuiltinAccess::Unknown;
|
||||
}
|
||||
|
||||
struct ImageUniformDecl {
|
||||
String name;
|
||||
String writeName; // the writeonly half's name, when split
|
||||
String layout; // raw contents of layout(...)
|
||||
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
||||
String type; // image2D, uimage2DArray, ...
|
||||
String arraySuffix; // "" or "[7]"
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
SizeT referenceCount = 0; // uses this pass recognized and accounted for
|
||||
Bool loaded = false;
|
||||
Bool stored = false;
|
||||
Bool unknownUse = false;
|
||||
Bool split = false;
|
||||
};
|
||||
|
||||
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
||||
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
||||
// comes out of here, whichever order the two passes end up running in.
|
||||
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
||||
const String& variableName) {
|
||||
String out = "layout(" + decl.layout + ") uniform ";
|
||||
out += memoryQualifier;
|
||||
out += ' ';
|
||||
if (!decl.qualifiers.empty()) {
|
||||
out += decl.qualifiers;
|
||||
out += ' ';
|
||||
}
|
||||
out += decl.type;
|
||||
out += ' ';
|
||||
out += variableName;
|
||||
out += decl.arraySuffix;
|
||||
out += ';';
|
||||
return out;
|
||||
}
|
||||
|
||||
// A name for the writeonly half that no identifier in the shader (and no other
|
||||
// half already minted) can collide with.
|
||||
String MakeImageWriteAliasName(const String& name, const String& source,
|
||||
const Vector<String>& taken) {
|
||||
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
|
||||
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
||||
// that already starts with '_' would otherwise produce.
|
||||
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
||||
doubled = candidate.find("__", doubled)) {
|
||||
candidate.erase(doubled, 1);
|
||||
}
|
||||
auto isTaken = [&](const String& identifier) {
|
||||
if (ContainsIdentifier(source, identifier)) return true;
|
||||
for (const auto& other : taken) {
|
||||
if (other == identifier) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (isTaken(candidate)) candidate += 'X';
|
||||
return candidate;
|
||||
}
|
||||
|
||||
struct ImageSourceEdit {
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
String text;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
String SplitReadWriteImageUniforms(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (glslCode.find("image") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// layout(...) uniform <memory/precision qualifiers> <image type> <name>[array];
|
||||
// The qualifier alternation is order-free even though SPIRV-Cross emits a fixed
|
||||
// order (to_qualifiers_glsl: storage, then coherent/restrict/readonly/writeonly,
|
||||
// then precision), and the array group is repeated so a hypothetical multi-
|
||||
// dimensional image array survives the round trip intact.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*((?:\[[^\]]*\]\s*)*);)");
|
||||
|
||||
Vector<ImageUniformDecl> decls;
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
const String qualifiers = match[2].str();
|
||||
// Already legal: SPIRV-Cross decided one way, leave it alone.
|
||||
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bool hasFormat = false;
|
||||
Bool exemptFormat = false;
|
||||
for (const String& token : SplitLayoutQualifierList(match[1].str())) {
|
||||
if (!IsImageFormatQualifier(token)) continue;
|
||||
hasFormat = true;
|
||||
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
|
||||
}
|
||||
// No format qualifier at all is a different (and, in ES, unconditionally
|
||||
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
|
||||
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
|
||||
if (!hasFormat || exemptFormat) continue;
|
||||
|
||||
ImageUniformDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
decl.qualifiers = NormalizeDeclarationSpacing(qualifiers);
|
||||
decl.type = match[3].str();
|
||||
decl.name = match[4].str();
|
||||
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
if (decls.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
auto findDecl = [&decls](const String& name) -> SizeT {
|
||||
for (SizeT i = 0; i < decls.size(); ++i) {
|
||||
if (decls[i].name == name) return i;
|
||||
}
|
||||
return decls.size();
|
||||
};
|
||||
|
||||
// Walk every `image*(` call and attribute its first argument to a declaration.
|
||||
struct StoreSite {
|
||||
SizeT declIndex;
|
||||
SizeT start;
|
||||
SizeT length;
|
||||
};
|
||||
Vector<StoreSite> storeSites;
|
||||
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
||||
SizeT tokenEnd = pos;
|
||||
while (tokenEnd < glslCode.size() && IsImagePassIdentifierChar(glslCode[tokenEnd])) ++tokenEnd;
|
||||
const String builtin = glslCode.substr(pos, tokenEnd - pos);
|
||||
|
||||
const SizeT openParen = glslCode.find_first_not_of(" \t\r\n", tokenEnd);
|
||||
if (openParen == String::npos || glslCode[openParen] != '(') continue; // a type, not a call
|
||||
|
||||
const SizeT argStart = glslCode.find_first_not_of(" \t\r\n", openParen + 1);
|
||||
if (argStart == String::npos) continue;
|
||||
if (!std::isalpha(static_cast<unsigned char>(glslCode[argStart])) && glslCode[argStart] != '_') {
|
||||
continue; // an expression, not a bare variable - it names no image of ours
|
||||
}
|
||||
SizeT argEnd = argStart;
|
||||
while (argEnd < glslCode.size() && IsImagePassIdentifierChar(glslCode[argEnd])) ++argEnd;
|
||||
|
||||
const SizeT declIndex = findDecl(glslCode.substr(argStart, argEnd - argStart));
|
||||
if (declIndex == decls.size()) continue;
|
||||
ImageUniformDecl& decl = decls[declIndex];
|
||||
++decl.referenceCount;
|
||||
|
||||
// The operand has to be the bare variable, optionally subscripted. Anything
|
||||
// else (a member access, a call result) is a shape this pass cannot rewrite.
|
||||
SizeT after = glslCode.find_first_not_of(" \t\r\n", argEnd);
|
||||
if (after != String::npos && glslCode[after] == '[') {
|
||||
Int depth = 0;
|
||||
SizeT scan = after;
|
||||
for (; scan < glslCode.size(); ++scan) {
|
||||
if (glslCode[scan] == '[') ++depth;
|
||||
else if (glslCode[scan] == ']' && --depth == 0) break;
|
||||
}
|
||||
after = scan >= glslCode.size() ? String::npos
|
||||
: glslCode.find_first_not_of(" \t\r\n", scan + 1);
|
||||
}
|
||||
const char nextChar = after == String::npos ? '\0' : glslCode[after];
|
||||
if (nextChar != ',' && nextChar != ')') {
|
||||
decl.unknownUse = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (ClassifyImageBuiltin(builtin)) {
|
||||
case ImageBuiltinAccess::Load:
|
||||
decl.loaded = true;
|
||||
break;
|
||||
case ImageBuiltinAccess::Store:
|
||||
decl.stored = true;
|
||||
storeSites.push_back({declIndex, argStart, argEnd - argStart});
|
||||
break;
|
||||
case ImageBuiltinAccess::None:
|
||||
break;
|
||||
default:
|
||||
decl.unknownUse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Every mention of the name has to be one this pass saw, or the split would leave
|
||||
// a store pointing at the readonly half. One occurrence is the declaration itself.
|
||||
for (auto& decl : decls) {
|
||||
if (CountIdentifierOccurrences(glslCode, decl.name) != decl.referenceCount + 1) {
|
||||
decl.unknownUse = true;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<ImageSourceEdit> edits;
|
||||
Vector<String> takenAliases;
|
||||
for (auto& decl : decls) {
|
||||
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
||||
if (decl.loaded && decl.stored) {
|
||||
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||
takenAliases.push_back(decl.writeName);
|
||||
decl.split = true;
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
|
||||
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
|
||||
} else if (decl.stored) {
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||
} else {
|
||||
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
||||
// the qualifier that keeps every one of those legal.
|
||||
edits.push_back({decl.declStart, decl.declLength,
|
||||
BuildImageDeclaration(decl, "readonly", decl.name)});
|
||||
}
|
||||
}
|
||||
for (const StoreSite& site : storeSites) {
|
||||
const ImageUniformDecl& decl = decls[site.declIndex];
|
||||
if (!decl.split) continue;
|
||||
edits.push_back({site.start, site.length, decl.writeName});
|
||||
}
|
||||
if (edits.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Back to front, so an earlier edit's offsets stay valid.
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
||||
String result = glslCode;
|
||||
for (const ImageSourceEdit& edit : edits) {
|
||||
result.replace(edit.start, edit.length, edit.text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How a lookup carries its level of detail, and how many arguments it takes
|
||||
// before the optional bias.
|
||||
@@ -527,7 +1060,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String EmulateTextureLodBias(const String& glslCode) {
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -588,6 +1121,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (samplerIt == samplerNames.end()) continue;
|
||||
|
||||
const String& biasName = samplerIt->second;
|
||||
if (form->explicitLodArg >= 0 && avoidExplicitLodBias) {
|
||||
// The lookup already names its level; leaving it alone keeps a constant
|
||||
// LOD constant. Costs the bias on explicit-LOD lookups only.
|
||||
continue;
|
||||
}
|
||||
if (form->explicitLodArg >= 0) {
|
||||
// Explicit LOD: the bias adds to it, as Vulkan does for
|
||||
// OpImageSampleExplicitLod and as the CTS reference expects.
|
||||
@@ -638,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
|
||||
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
MGLOG_D("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return (rowBytes + align - 1) / align * align;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
// Walks the client-side destination the PACK parameters describe and hands each row to
|
||||
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
|
||||
// texels. Shared by the converting and the raw-word stores so both address the destination -
|
||||
// and feed the bound pixel-pack buffer - identically.
|
||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||
// Per the GL addressing rules, slice k row j lands at
|
||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
template <typename FillRow>
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
fillRow(slice, row, convertedRow.data());
|
||||
|
||||
if (packParams.SwapBytes) {
|
||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||
if (groupSize > 1) {
|
||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||
if (packParams.SwapBytes && swapGroupSize > 1) {
|
||||
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixelPackBufferObject) {
|
||||
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||
// the buffer draw-clean memos (once for the whole row loop).
|
||||
@@ -1147,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
|
||||
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
|
||||
|
||||
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow =
|
||||
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
});
|
||||
}
|
||||
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
|
||||
return false;
|
||||
}
|
||||
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
|
||||
|
||||
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
|
||||
});
|
||||
}
|
||||
} // namespace ReadbackImpl
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -115,6 +115,16 @@ 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 {
|
||||
@@ -130,7 +140,68 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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);
|
||||
// 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 writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// 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:
|
||||
// * loaded only -> add `readonly`
|
||||
// * stored only -> add `writeonly`
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `readonly <name>` and `writeonly
|
||||
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
|
||||
// imageStore at the second one. 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.
|
||||
//
|
||||
// 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.
|
||||
String SplitReadWriteImageUniforms(const String& glslCode);
|
||||
// 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_";
|
||||
@@ -142,7 +213,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
String EmulateTextureLodBias(const String& glslCode);
|
||||
//
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -497,30 +497,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 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),
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, 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_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,
|
||||
// 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.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -539,6 +557,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
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.
|
||||
@@ -672,7 +699,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -768,14 +796,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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));
|
||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
||||
// 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.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||
@@ -843,7 +905,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// Never, on any device, and no longer for the reason it used to be. It used to track
|
||||
// shaderFloat64 because a `dvec3` input needed the Float64 capability 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.
|
||||
//
|
||||
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
||||
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
||||
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
||||
// input would be silent garbage. Reconstructing the value would mean decoding the
|
||||
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
||||
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
||||
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
||||
// dvec4 cannot even express within one attribute location.
|
||||
//
|
||||
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
||||
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
||||
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
||||
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
||||
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
|
||||
@@ -62,8 +62,8 @@ 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 shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). 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.
|
||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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 anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
parameterBuffer->SyncPersistentMappedRange();
|
||||
if (parameterBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -801,9 +801,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// already recorded the new binding on the program - which is what reseeds this cache
|
||||
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
|
||||
// (the common case: the very next draw reads it) from having to be thrown away.
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
//
|
||||
// Resolve the index BEFORE taking the reference, and bounds-check the way the
|
||||
// sibling getter does. GetShaderStorageBlockIndex re-enters GetProgramResourceCache,
|
||||
// which indexes g_programResourceCaches and can therefore insert - and that map is
|
||||
// open-addressed, so a rehash MOVES its entries and a reference taken before the
|
||||
// call is left dangling. Binding a program's storage block
|
||||
// while another program's entry was still absent from the cache was a reproducible
|
||||
// segfault (ProgramPipelineScenario's two storage-block cases, in one process).
|
||||
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
|
||||
if (blockIndex == GL_INVALID_INDEX) return;
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
if (blockIndex >= cache.storageBlocks.size()) return;
|
||||
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
||||
}
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
@@ -966,6 +975,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// With no element-array buffer bound, every indices[i] is a client pointer into a
|
||||
// separate CPU allocation, not an offset into one shared buffer. The batched payload
|
||||
// below cannot express that: it carries ONE index-buffer view for the whole batch and
|
||||
// turns each pointer into a firstIndex relative to it. Replay the sub-draws through
|
||||
// the single-draw entry point instead - it snapshots each client range into its own
|
||||
// transient slice, which is exactly what the unrolled draws this must match do.
|
||||
// (The batch used to be built this way; the shared-view rewrite that added
|
||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||
// declined the whole batch and painted nothing.)
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) {
|
||||
continue;
|
||||
}
|
||||
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex != nullptr ? basevertex[i] : 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -977,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// shift - the hardware divide was the hottest instruction of this loop.
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||
|
||||
@@ -111,6 +111,11 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,53 +194,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
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)));
|
||||
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.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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.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)));
|
||||
XXH64_update(m_hashState.Get(), &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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)));
|
||||
XXH64_update(m_hashState.Get(), &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXH64_update(m_hashState.Get(), &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
XXH64_update(m_hashState.Get(), &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
if (payload.colorAttachmentCount > 0) {
|
||||
XXHASH_VERIFY(XXH64_update(
|
||||
m_hashState,
|
||||
m_hashState.Get(),
|
||||
payload.colorBlendAttachments.data(),
|
||||
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
||||
@@ -252,6 +253,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
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, payload.renderPass,
|
||||
m_frameCounter});
|
||||
return pipeline;
|
||||
@@ -389,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
// 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;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
@@ -458,9 +480,56 @@ 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>(payload.stages->size());
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||
gpi.pStages = effectiveStages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
@@ -477,6 +546,13 @@ 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),
|
||||
@@ -507,6 +583,36 @@ 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",
|
||||
|
||||
@@ -12,8 +12,19 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.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;
|
||||
@@ -32,6 +43,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// 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;
|
||||
@@ -61,7 +80,20 @@ 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: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
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);
|
||||
@@ -134,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -32,7 +34,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CombinedImageSampler,
|
||||
UniformTexelBuffer,
|
||||
StorageBuffer,
|
||||
StorageImage
|
||||
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
|
||||
};
|
||||
|
||||
enum class CompileOptionBit : Uint {
|
||||
@@ -52,6 +60,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
@@ -62,6 +83,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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;
|
||||
@@ -75,8 +99,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> activeBindings;
|
||||
Vector<Uint32> dynamicBindings;
|
||||
Vector<Int> uniformBlockIndexByBinding;
|
||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||
// binding with descriptorCount = N).
|
||||
// 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).
|
||||
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.
|
||||
@@ -85,6 +110,11 @@ 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;
|
||||
@@ -92,6 +122,19 @@ 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{};
|
||||
@@ -104,6 +147,33 @@ 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.
|
||||
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;
|
||||
// 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).
|
||||
@@ -118,6 +188,14 @@ 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;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
@@ -136,6 +214,7 @@ 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;
|
||||
@@ -145,11 +224,16 @@ 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;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -157,6 +241,10 @@ 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.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -167,6 +255,7 @@ 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;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
@@ -185,6 +274,7 @@ 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;
|
||||
@@ -194,11 +284,16 @@ 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;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -206,6 +301,10 @@ 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.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -233,6 +332,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
modules.clear();
|
||||
stages.clear();
|
||||
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||
}
|
||||
};
|
||||
|
||||
@@ -256,13 +356,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
// 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(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MG_State::GLState::ProgramObject& 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
|
||||
@@ -291,6 +405,39 @@ 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 because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
|
||||
// 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);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -304,11 +451,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
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<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
Uint32 m_maxBindings = 0;
|
||||
@@ -320,12 +476,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// 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;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
}
|
||||
|
||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,10 +53,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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 binding (post fallback substitution,
|
||||
// in binding 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).
|
||||
// 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;
|
||||
@@ -143,8 +146,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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 MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
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):
|
||||
@@ -152,27 +156,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) 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).
|
||||
// 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 MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
VkDescriptorImageInfo& outImageInfo,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint = false) const;
|
||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
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 MG_State::GLState::ProgramObject& 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 MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
// `element` indexes an image ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL image unit.
|
||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Uint32 element, 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 {
|
||||
@@ -319,8 +341,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.
|
||||
// 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.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
@@ -341,6 +366,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
|
||||
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||
const auto& attr = vao.GetAttribute(i);
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Enabled, sizeof(attr.Enabled)));
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
|
||||
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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Normalized, sizeof(attr.Normalized)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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
|
||||
@@ -45,10 +45,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
MGLOG_E_ONCE("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);
|
||||
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
MGLOG_W_ONCE("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),
|
||||
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
MGLOG_E_ONCE("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);
|
||||
@@ -146,15 +146,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
MGLOG_E_ONCE("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;
|
||||
}
|
||||
|
||||
const Uint32 sourceStride =
|
||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||
// 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 Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
@@ -169,16 +175,22 @@ 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("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
MGLOG_W_ONCE("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;
|
||||
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)));
|
||||
// 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) {
|
||||
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;
|
||||
@@ -213,24 +225,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), 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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &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.Get(), &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState.Get());
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -111,10 +112,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
||||
// so INSERT invalidates references to stored values. The draw path (and
|
||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
||||
// only the unique_ptr cell moves, never the pointee.
|
||||
// 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;
|
||||
@@ -124,6 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
// "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
|
||||
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
|
||||
// invalid unless the buffer was created with this bit. Nothing asked for it until
|
||||
// imageBuffer support existed, so the omission was invisible.
|
||||
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||
@@ -161,12 +165,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferManager::CollectAllDeferredReleases() {
|
||||
// Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
|
||||
// so no memo can still name the handle, and the caller has proved the GPU is idle.
|
||||
//
|
||||
// The transient arena's releases are deliberately NOT collected here. A buffer lands
|
||||
// there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
|
||||
// that moment every slice already handed out from this frame's arena still names it -
|
||||
// VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
|
||||
// serving for the whole frame serial on the strength of transientFrameSerial alone.
|
||||
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
|
||||
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
|
||||
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
|
||||
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
|
||||
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
|
||||
// the slot's slices stop being reachable, and that is where these releases land.
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||
CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void VkBufferManager::NotifyDeviceIdle() {
|
||||
@@ -287,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.requiredFlags = requiredFlags,
|
||||
});
|
||||
if (!created || resource.buffer.Map() == nullptr) {
|
||||
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
static_cast<unsigned long long>(size));
|
||||
resource.buffer.Destroy();
|
||||
resource.storageSize = 0;
|
||||
@@ -309,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
@@ -368,6 +383,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BumpSliceEpoch(*resource);
|
||||
// Any cached streaming slice refers to the previous contents.
|
||||
resource->transientFrameSerial = 0;
|
||||
// Redefining the store hands any adopted mapping back to the CPU shadow
|
||||
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
|
||||
// is an ordinary resident one again: it needs the busy-tracking and conditional
|
||||
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
|
||||
// store rather than hand back a mapping of the old one.
|
||||
resource->persistentMapped = false;
|
||||
if (!resource->buffer.IsValid()) {
|
||||
return; // streaming-only resource: shadow + serial are enough
|
||||
}
|
||||
@@ -388,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
}
|
||||
@@ -413,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -450,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -542,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -564,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
resource->buffer.Destroy();
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
@@ -599,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -697,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case BufferKind::Uniform:
|
||||
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
||||
case BufferKind::TextureBuffer:
|
||||
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
|
||||
// Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
|
||||
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
|
||||
// the two it is only becomes known when a shader that uses it is bound - long after
|
||||
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
|
||||
// invalid unless the buffer was created with the storage bit, so a buffer that
|
||||
// acquired only the uniform bit could never be given one.
|
||||
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
|
||||
case BufferKind::ShaderStorage:
|
||||
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
||||
case BufferKind::Indirect:
|
||||
|
||||
@@ -102,10 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||
// transient arena's parked superseded blocks). Only valid when the
|
||||
// caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
// 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();
|
||||
|
||||
@@ -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("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
MGLOG_E_ONCE("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("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
MGLOG_E_ONCE("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("VkBufferObject::Upload failed: unable to map buffer");
|
||||
MGLOG_E_ONCE("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("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
if (!wasMapped) {
|
||||
Unmap();
|
||||
}
|
||||
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!attachment.IsComplete()) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = attachment.GetTexture().get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
renderbuffer->GetSamples(),
|
||||
renderbuffer->GetExternalIndex());
|
||||
return nullptr;
|
||||
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||
&imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
static_cast<Int>(format),
|
||||
static_cast<Int>(sampleCount),
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -594,27 +594,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||
Bool includeDefaultFboDepthStencil) {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config.CacheVersion));
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
}
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
auto readBuffer = fbo.GetReadBuffer();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &readBuffer, sizeof(FramebufferAttachmentType)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &readBuffer, sizeof(FramebufferAttachmentType)));
|
||||
Int validDrawBufCount = 0;
|
||||
for (Int i = 0; i < drawBuffers.size(); ++i) {
|
||||
auto drawbuf = drawBuffers[i];
|
||||
if (drawbuf != FramebufferAttachmentType::None)
|
||||
validDrawBufCount = std::max(validDrawBufCount, i + 1);
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &validDrawBufCount, sizeof(validDrawBufCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &validDrawBufCount, sizeof(validDrawBufCount)));
|
||||
|
||||
auto combineFramebufferAttachmentObjHash = [&](FramebufferAttachmentType attachment) {
|
||||
auto& att = fbo.GetAttachment(attachment);
|
||||
@@ -623,49 +623,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsEmpty()) type = 0;
|
||||
else if (att.IsTexture()) type = 1;
|
||||
else if (att.IsRenderbuffer()) type = 2;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &type, sizeof(type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &type, sizeof(type)));
|
||||
void* contentPtr = nullptr;
|
||||
if (att.IsTexture())
|
||||
contentPtr = att.GetTexture().get();
|
||||
else if (att.IsRenderbuffer())
|
||||
contentPtr = att.GetRenderbuffer().get();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &contentPtr, sizeof(contentPtr)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &contentPtr, sizeof(contentPtr)));
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &textureLayered, sizeof(textureLayered)));
|
||||
|
||||
Uint64 imageIdentity = 0;
|
||||
auto* texture = att.GetTexture().get();
|
||||
auto* resource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
if (resource != nullptr) {
|
||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
} else {
|
||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity)));
|
||||
}
|
||||
|
||||
if (includePendingClear && att.IsTexture()) {
|
||||
auto* texture = att.GetTexture().get();
|
||||
const auto pendingClearKey = VkClearManager::MakePendingClearKey(att);
|
||||
auto hasClear = m_clearManager.HasPendingClear(pendingClearKey);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear)));
|
||||
if (hasClear) {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
Bool hasPayload = m_clearManager.GetPendingClear(pendingClearKey, clearPayload);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload)));
|
||||
if (hasPayload) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,7 +695,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentLayout = textureResource->layout;
|
||||
}
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout)));
|
||||
}
|
||||
if (att.IsRenderbuffer() && att.GetRenderbuffer()) {
|
||||
const auto& renderbuffer = att.GetRenderbuffer();
|
||||
@@ -703,10 +703,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int width = renderbuffer->GetWidth();
|
||||
const Int height = renderbuffer->GetHeight();
|
||||
const Int samples = renderbuffer->GetSamples();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &internalFormat, sizeof(internalFormat)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &width, sizeof(width)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &height, sizeof(height)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &samples, sizeof(samples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &internalFormat, sizeof(internalFormat)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &width, sizeof(width)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &height, sizeof(height)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &samples, sizeof(samples)));
|
||||
|
||||
Uint64 imageIdentity = 0;
|
||||
VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -714,25 +714,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource != nullptr) {
|
||||
imageIdentity = reinterpret_cast<Uint64>(resource->image);
|
||||
currentLayout = resource->layout;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &resource->sampleCount, sizeof(resource->sampleCount)));
|
||||
} else {
|
||||
const VkSampleCountFlagBits fallbackSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &fallbackSampleCount, sizeof(fallbackSampleCount)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &imageIdentity, sizeof(imageIdentity)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &imageIdentity, sizeof(imageIdentity)));
|
||||
|
||||
if (includePendingClear) {
|
||||
const Bool hasClear = HasPendingRenderbufferClear(att);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasClear, sizeof(hasClear)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasClear, sizeof(hasClear)));
|
||||
if (hasClear) {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
const Bool hasPayload = GetPendingRenderbufferClear(renderbuffer.get(), clearPayload);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &hasPayload, sizeof(hasPayload)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &hasPayload, sizeof(hasPayload)));
|
||||
if (hasPayload) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &clearPayload.mask, sizeof(clearPayload.mask)));
|
||||
}
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, ¤tLayout, sizeof(currentLayout)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), ¤tLayout, sizeof(currentLayout)));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -745,13 +745,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||
// entirely, so it must hash differently from the depth-full flavor.
|
||||
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
if (depthStencilIncluded) {
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||
continue;
|
||||
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
|
||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
"using LOAD_OP_DONT_CARE",
|
||||
texture->GetExternalIndex());
|
||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
if (hasDistinctDepthAndStencilAttachments) {
|
||||
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
fbo.GetExternalIndex());
|
||||
}
|
||||
if (selectedDepthStencilAttachment != nullptr) {
|
||||
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
||||
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||
depthAttachmentId);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <unordered_map>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
@@ -101,6 +102,42 @@ 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,
|
||||
@@ -315,26 +352,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
|
||||
// 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. FastSTL's
|
||||
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
|
||||
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
|
||||
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
|
||||
// relocation the cached pointer names freed storage still holding the pre-clear
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails 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.
|
||||
// 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. This buys stability
|
||||
// across rehash and insert only - erase still invalidates the erased element, which is safe
|
||||
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
|
||||
// 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*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
@@ -351,7 +392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
static inline Bool s_hasActiveRenderPass = false;
|
||||
static inline VkClearManager* s_clearManager = nullptr;
|
||||
|
||||
@@ -134,41 +134,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState.Get(), m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minFilter, sizeof(minFilter)));
|
||||
const auto magFilter = sampler.GetMagFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &magFilter, sizeof(magFilter)));
|
||||
const auto mipmapMode = sampler.GetMipmapMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &mipmapMode, sizeof(mipmapMode)));
|
||||
const auto wrapS = sampler.GetWrapS();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapS, sizeof(wrapS)));
|
||||
const auto wrapT = sampler.GetWrapT();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxLod, sizeof(maxLod)));
|
||||
const auto lodBias = sampler.GetLodBias();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &lodBias, sizeof(lodBias)));
|
||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &compareFunc, sizeof(compareFunc)));
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState.Get(), &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState.Get());
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include "../VulkanRendererConfig.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
@@ -85,6 +86,6 @@ private:
|
||||
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();
|
||||
static inline MobileGL::XXH64State m_hashState;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||
|
||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
||||
oldResource.arrayLayers);
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||
|
||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||
@@ -950,7 +947,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode());
|
||||
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
||||
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
||||
if (perMipSampledView == VK_NULL_HANDLE) {
|
||||
@@ -974,13 +972,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return resource->sampledView;
|
||||
}
|
||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1000,7 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
"for textureId=%d (available=0x%x)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1014,7 +1012,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1042,14 +1040,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
format = resource->format;
|
||||
}
|
||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format != resource->format &&
|
||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1069,7 +1067,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
__func__, texture.GetExternalIndex());
|
||||
return VK_NULL_HANDLE;
|
||||
default:
|
||||
@@ -1078,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
if (viewType != resource->viewType) {
|
||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -1113,7 +1111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
"(available=0x%x)",
|
||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1124,7 +1122,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1190,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool lowerTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
||||
resource.aspect, 0, writtenMipLevel);
|
||||
MOBILEGL_ASSERT(lowerTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1202,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool upperTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
||||
resource.arrayLayers);
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||
MOBILEGL_ASSERT(upperTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1222,7 +1219,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
|
||||
@@ -1256,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
@@ -1287,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1354,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 layerCount) {
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||
Uint32 levelCount) {
|
||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||
@@ -1380,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||
barrier.subresourceRange.levelCount = levelCount;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = layerCount;
|
||||
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -1573,7 +1575,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||
// assertion would take the whole process down instead.
|
||||
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
"mipLevels=%u vkViewType=%d",
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
||||
@@ -1802,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||
// this texture; failing creation would lose the texture entirely, so retry
|
||||
// as a plain immutable-format image.
|
||||
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||
"will be unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1820,7 +1822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1852,7 +1854,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult createImageResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||
if (createImageResult != VK_SUCCESS) {
|
||||
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
|
||||
// recovers from, and it re-fires on every sync of every texture the driver refuses.
|
||||
MGLOG_E_ONCE("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
"mips=%u samples=%d format=%d",
|
||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||
@@ -1990,7 +1994,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Bound the idle pool: a one-off giant upload (initial atlas define)
|
||||
// must not pin its staging memory forever.
|
||||
constexpr VkDeviceSize kMaxFreeUploadStagingBytes = 32u * 1024u * 1024u;
|
||||
if (m_allocator == nullptr || m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) {
|
||||
if (m_allocator == nullptr) {
|
||||
// The normal shutdown path destroys the free list through
|
||||
// DestroyUploadPools while the allocator is still valid, so this is a
|
||||
// defensive backstop only. Never pass a null allocator to VMA.
|
||||
MGLOG_W_ONCE("VkTextureManager::RecycleUploadStagingBlock called with a null allocator");
|
||||
return;
|
||||
}
|
||||
if (m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) {
|
||||
vmaDestroyBuffer(m_allocator, block.buffer, block.allocation);
|
||||
return;
|
||||
}
|
||||
@@ -2238,7 +2249,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.fullView == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect);
|
||||
const VkImageAspectFlags sampledAspect =
|
||||
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
|
||||
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
|
||||
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
||||
if (resource.sampledView == VK_NULL_HANDLE) {
|
||||
@@ -2424,7 +2436,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
||||
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
MGLOG_E_ONCE("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
@@ -2601,7 +2613,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
uploadSrcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
@@ -2705,7 +2717,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||
outResource.layout = finalLayout;
|
||||
|
||||
@@ -2860,10 +2872,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) {
|
||||
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
|
||||
GLenum depthStencilTextureMode) {
|
||||
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
}
|
||||
// A sampled view of a combined depth/stencil image may name exactly one aspect
|
||||
// (VUID-VkDescriptorImageInfo-imageView-01976), and GL_DEPTH_STENCIL_TEXTURE_MODE is
|
||||
// what picks it - the whole content of GL_ARB_stencil_texturing. Depth stays the
|
||||
// default, so nothing that never sets the mode changes shape. The texture's params
|
||||
// version moves with the mode, which is what makes the cached views be rebuilt.
|
||||
if (depthStencilTextureMode == GL_STENCIL_INDEX && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
}
|
||||
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
@@ -379,17 +379,33 @@ public:
|
||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
||||
// `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 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 layerCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
|
||||
@@ -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("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
MGLOG_W_ONCE("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("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("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("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
MGLOG_W_ONCE("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("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
return false;
|
||||
}
|
||||
if (resultWithAvailability[1] == 0) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -211,10 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
//
|
||||
// `defaultFramebufferOrientation` is set only when the source is the swapchain's
|
||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
||||
// the way back, exactly as the colour ReadPixels path does.
|
||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels);
|
||||
void* pixels, Bool defaultFramebufferOrientation = false);
|
||||
// Same-extent depth blit between images of different depth formats: host
|
||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||
@@ -224,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||
Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize, Uint8* outPixels);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -293,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||
}
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -363,6 +386,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 samplerBinding = 0;
|
||||
};
|
||||
|
||||
// A single-sample staging image for multisample-resolve blits that also have to change
|
||||
// orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
|
||||
// invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
|
||||
// framebuffer used to land the mirrored band. Resolving here first and then blitting from
|
||||
// here separates the two operations, and each one then does only what it can express.
|
||||
//
|
||||
// Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
|
||||
// create-destroy per call would both cost allocations and, worse, need per-call deferred
|
||||
// destruction to outlive the recording. It grows to the largest extent asked for and is
|
||||
// reused; format changes recreate it.
|
||||
struct MultisampleResolveScratchImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D extent = {0, 0};
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
MultisampleResolveScratchImage m_msResolveScratch;
|
||||
// Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
|
||||
// TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
|
||||
// back to the direct resolve).
|
||||
Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
|
||||
VkExtent2D extent);
|
||||
void DestroyMultisampleResolveScratchImage();
|
||||
|
||||
struct DeferredDepthMipmapCleanup {
|
||||
Vector<VkImageView> imageViews;
|
||||
Vector<VkFramebuffer> framebuffers;
|
||||
@@ -445,15 +493,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
||||
// CreateInstance() from the enumerated instance extensions. On desktop an
|
||||
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
||||
// a substituted window. (Android is the one exception and has its own path -
|
||||
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
|
||||
// context is given an AImageReader ANativeWindow that is never displayed.)
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||
@@ -519,6 +565,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||
Bool m_multiViewportFeatureEnabled = false;
|
||||
Uint32 m_maxRasterizableViewports = 1;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
@@ -745,6 +797,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Does the current program's vertex stage declare the BaseVertex builtin? A property
|
||||
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
|
||||
//
|
||||
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
|
||||
// variant, and a program that only ever draws non-indexed would then compile a variant
|
||||
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
|
||||
// it. With the memo the answer is known before the first lookup and only the variant
|
||||
// the draw actually needs is resolved.
|
||||
Bool m_lastBaseVertexQueryValid = false;
|
||||
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
|
||||
Uint32 m_lastBaseVertexProgramVersion = 0;
|
||||
Bool m_lastBaseVertexReads = false;
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
@@ -789,6 +854,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
@@ -1079,7 +1149,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||
// rasterizable viewport count, which takes the unmemoized array path.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||
Uint32 viewportCount = 1);
|
||||
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||
// disagree.
|
||||
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -1120,6 +1205,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool MaterializePendingClearForRenderbuffer(
|
||||
VkCommandBuffer commandBuffer,
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
// The default framebuffer's twin of the two above. It cannot go through
|
||||
// MaterializePendingClearForTexture: the default FBO's colour attachment is a
|
||||
// placeholder texture object, and syncing THAT would clear a texture image nobody
|
||||
// presents instead of the acquired swapchain image.
|
||||
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::FramebufferObject& fbo,
|
||||
FramebufferAttachmentType attachmentType);
|
||||
// Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
|
||||
// different clear command and per-aspect masking.
|
||||
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const ClearAttachmentPayload& payload);
|
||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
|
||||
@@ -74,6 +74,18 @@ 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); \
|
||||
|
||||
@@ -42,4 +42,5 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
add_subdirectory(Driver)
|
||||
add_subdirectory(Driver)
|
||||
add_subdirectory(Container)
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
add_executable(
|
||||
UnorderedMapBench
|
||||
UnorderedMapBench.cpp
|
||||
)
|
||||
|
||||
target_include_directories(UnorderedMapBench PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
UnorderedMapBench PRIVATE
|
||||
benchmark::benchmark
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_test(NAME UnorderedMapBench COMMAND UnorderedMapBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(UnorderedMapBench PROPERTIES LABELS benchmark)
|
||||
@@ -0,0 +1,248 @@
|
||||
// MobileGL - MobileGL/MG_Benchmark/Container/UnorderedMapBench.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 standing performance observatory for MobileGL::UnorderedMap.
|
||||
//
|
||||
// This benchmarks the ALIAS, never a concrete table, so whatever UnorderedMap
|
||||
// names today is what gets measured - swap the container in MG_Util/Types.h and
|
||||
// re-run this same binary to get a directly comparable set of numbers. That is
|
||||
// the point of it: the container sits on per-draw paths, so a change to it needs
|
||||
// evidence, and the evidence should be produced the same way every time.
|
||||
//
|
||||
// The workloads are the shapes the tree actually exercises, not generic hash-map
|
||||
// microbenchmarks. Four key shapes, because they stress a hash function very
|
||||
// differently:
|
||||
// * SEQUENTIAL dense small integers - GL object names from the index generator
|
||||
// (buffer/texture/framebuffer/sampler registries).
|
||||
// * POINTER real heap addresses - StateBackendObjectRegistry keys on
|
||||
// StateObject*. These are aligned, so their low bits are the
|
||||
// least random part of the key; a table that indexes on raw low
|
||||
// bits clusters badly here and one that mixes first does not.
|
||||
// Taken from the real allocator rather than a synthetic stride,
|
||||
// which would flatter whichever table mixes its bits.
|
||||
// * DIGEST already well-mixed 64-bit values - the XXH64 pipeline,
|
||||
// vertex-input-state and program memos.
|
||||
// * NAME short strings - uniform/attribute name to location maps.
|
||||
//
|
||||
// Sizes sweep from 8 upward because the per-draw memos are usually SMALL; a table
|
||||
// that only wins at 4096 entries has not won anything that matters here.
|
||||
//
|
||||
// Run: build-linux/MobileGL/MG_Benchmark/Container/UnorderedMapBench
|
||||
// or: ctest -R UnorderedMapBench (label: benchmark)
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "MG_Util/Types.h"
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Int64 kMinSize = 8;
|
||||
constexpr Int64 kMaxSize = 4096;
|
||||
|
||||
// Keep the real allocations alive for the whole process: the POINTER shape is
|
||||
// only honest if the keys are addresses the allocator actually handed out, and
|
||||
// they have to stay unique (a freed address can be handed out twice).
|
||||
std::vector<std::unique_ptr<char[]>>& PointerKeyStorage() {
|
||||
static std::vector<std::unique_ptr<char[]>> storage;
|
||||
return storage;
|
||||
}
|
||||
|
||||
Vector<Uint64> SequentialKeys(SizeT n) {
|
||||
Vector<Uint64> keys;
|
||||
keys.reserve(n);
|
||||
for (SizeT i = 0; i < n; ++i) keys.push_back(static_cast<Uint64>(i) + 1);
|
||||
return keys;
|
||||
}
|
||||
|
||||
Vector<Uint64> PointerKeys(SizeT n) {
|
||||
auto& storage = PointerKeyStorage();
|
||||
Vector<Uint64> keys;
|
||||
keys.reserve(n);
|
||||
std::mt19937_64 rng(0xBEEF);
|
||||
std::vector<std::unique_ptr<char[]>> churn;
|
||||
for (SizeT i = 0; i < n; ++i) {
|
||||
// State objects are not all one size, and the allocator sees other
|
||||
// traffic between them - a single uniform stride is not what this
|
||||
// registry ever sees.
|
||||
const SizeT sz = 96 + (rng() % 192);
|
||||
auto p = std::make_unique<char[]>(sz);
|
||||
keys.push_back(reinterpret_cast<Uint64>(p.get()));
|
||||
storage.push_back(std::move(p));
|
||||
if ((rng() & 3) == 0) churn.push_back(std::make_unique<char[]>(32 + (rng() % 128)));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
Vector<Uint64> DigestKeys(SizeT n) {
|
||||
Vector<Uint64> keys;
|
||||
keys.reserve(n);
|
||||
std::mt19937_64 rng(0xC0FFEE);
|
||||
for (SizeT i = 0; i < n; ++i) keys.push_back(rng());
|
||||
return keys;
|
||||
}
|
||||
|
||||
Vector<String> NameKeys(SizeT n) {
|
||||
static const char* kPrefixes[] = {"u_", "a_", "mc_", "iris_", "gl_", "v_"};
|
||||
Vector<String> keys;
|
||||
keys.reserve(n);
|
||||
for (SizeT i = 0; i < n; ++i) {
|
||||
keys.push_back(String(kPrefixes[i % 6]) + "Uniform" + std::to_string(i) + "_xyz");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Key sets are built once per size and shared: generating them inside the timed
|
||||
// loop would measure the generator (and, for POINTER, the allocator) instead of
|
||||
// the table.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
const KeyVec& CachedKeys(SizeT n) {
|
||||
static UnorderedMap<SizeT, KeyVec> cache;
|
||||
auto it = cache.find(n);
|
||||
if (it != cache.end()) return it->second;
|
||||
return cache.emplace(n, Make(n)).first->second;
|
||||
}
|
||||
|
||||
template <typename Key>
|
||||
UnorderedMap<Key, Uint64> Populated(const Vector<Key>& keys) {
|
||||
UnorderedMap<Key, Uint64> map;
|
||||
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---- the workloads ----------------------------------------------------
|
||||
|
||||
// The dominant per-draw operation by a wide margin: a populated cache that is
|
||||
// read far more often than it is written.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void LookupHit(benchmark::State& state) {
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
|
||||
auto map = Populated(keys);
|
||||
for (auto _ : state) {
|
||||
for (const auto& k : keys) {
|
||||
auto it = map.find(k);
|
||||
benchmark::DoNotOptimize(it->second);
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
|
||||
}
|
||||
|
||||
// "Is this resource cached yet?" answered NO - the probe length on a miss is a
|
||||
// different cost from a hit, and resource caches ask this constantly.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void LookupMiss(benchmark::State& state) {
|
||||
const SizeT n = static_cast<SizeT>(state.range(0));
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(n);
|
||||
auto map = Populated(keys);
|
||||
const KeyVec absent = Make(n); // same shape, never inserted
|
||||
for (auto _ : state) {
|
||||
for (const auto& k : absent) {
|
||||
benchmark::DoNotOptimize(map.find(k) != map.end());
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(absent.size()));
|
||||
}
|
||||
|
||||
// Building a cache from empty, rehashes included.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void InsertGrow(benchmark::State& state) {
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
|
||||
for (auto _ : state) {
|
||||
UnorderedMap<typename KeyVec::value_type, Uint64> map;
|
||||
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
|
||||
benchmark::DoNotOptimize(map.size());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
|
||||
}
|
||||
|
||||
// Cache eviction and refill: erase half by key, put them back. This is the
|
||||
// aged-out-entry sweep the pipeline and vertex-input caches do.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void EraseChurn(benchmark::State& state) {
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
|
||||
for (auto _ : state) {
|
||||
state.PauseTiming();
|
||||
auto map = Populated(keys);
|
||||
state.ResumeTiming();
|
||||
for (SizeT i = 0; i < keys.size(); i += 2) benchmark::DoNotOptimize(map.erase(keys[i]));
|
||||
for (SizeT i = 0; i < keys.size(); i += 2) map[keys[i]] = i;
|
||||
benchmark::DoNotOptimize(map.size());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
|
||||
}
|
||||
|
||||
// Mass eviction: erase-while-iterating across the whole table. This is the loop
|
||||
// shape that a container's erase()-return contract can get wrong, and the one
|
||||
// that fed garbage handles to vkDestroyPipeline when it was wrong before.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void EraseSweep(benchmark::State& state) {
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
|
||||
for (auto _ : state) {
|
||||
state.PauseTiming();
|
||||
auto map = Populated(keys);
|
||||
state.ResumeTiming();
|
||||
for (auto it = map.begin(); it != map.end();) it = map.erase(it);
|
||||
benchmark::DoNotOptimize(map.size());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
|
||||
}
|
||||
|
||||
// Whole-table walks: the per-frame sweeps that age entries out, and the
|
||||
// teardown loops that destroy every Vulkan object a cache owns.
|
||||
template <typename KeyVec, KeyVec (*Make)(SizeT)>
|
||||
void Iterate(benchmark::State& state) {
|
||||
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
|
||||
auto map = Populated(keys);
|
||||
for (auto _ : state) {
|
||||
Uint64 acc = 0;
|
||||
for (const auto& entry : map) acc += entry.second;
|
||||
benchmark::DoNotOptimize(acc);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#define MGL_MAP_BENCH(WORKLOAD, SHAPE, VEC, MAKER) \
|
||||
BENCHMARK_TEMPLATE(WORKLOAD, VEC, MAKER) \
|
||||
->Name(#WORKLOAD "/" #SHAPE) \
|
||||
->RangeMultiplier(8) \
|
||||
->Range(kMinSize, kMaxSize)
|
||||
|
||||
MGL_MAP_BENCH(LookupHit, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(LookupHit, pointer, Vector<Uint64>, PointerKeys);
|
||||
MGL_MAP_BENCH(LookupHit, digest, Vector<Uint64>, DigestKeys);
|
||||
MGL_MAP_BENCH(LookupHit, name, Vector<String>, NameKeys);
|
||||
|
||||
MGL_MAP_BENCH(LookupMiss, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(LookupMiss, pointer, Vector<Uint64>, PointerKeys);
|
||||
MGL_MAP_BENCH(LookupMiss, digest, Vector<Uint64>, DigestKeys);
|
||||
MGL_MAP_BENCH(LookupMiss, name, Vector<String>, NameKeys);
|
||||
|
||||
MGL_MAP_BENCH(InsertGrow, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(InsertGrow, pointer, Vector<Uint64>, PointerKeys);
|
||||
MGL_MAP_BENCH(InsertGrow, digest, Vector<Uint64>, DigestKeys);
|
||||
MGL_MAP_BENCH(InsertGrow, name, Vector<String>, NameKeys);
|
||||
|
||||
MGL_MAP_BENCH(EraseChurn, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(EraseChurn, digest, Vector<Uint64>, DigestKeys);
|
||||
MGL_MAP_BENCH(EraseChurn, name, Vector<String>, NameKeys);
|
||||
|
||||
MGL_MAP_BENCH(EraseSweep, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(EraseSweep, digest, Vector<Uint64>, DigestKeys);
|
||||
|
||||
MGL_MAP_BENCH(Iterate, sequential, Vector<Uint64>, SequentialKeys);
|
||||
MGL_MAP_BENCH(Iterate, digest, Vector<Uint64>, DigestKeys);
|
||||
|
||||
BENCHMARK_MAIN();
|
||||
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
EGLStateContext* GetState() {
|
||||
if (!MG_State::pEGLContext) {
|
||||
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
|
||||
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
|
||||
}
|
||||
return MG_State::pEGLContext.get();
|
||||
}
|
||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
||||
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (releaseCurrentRequest) {
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
return EGL_FALSE;
|
||||
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
dpy, draw, read, ctx);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
width = std::max<EGLint>(width, 1);
|
||||
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", name);
|
||||
MGLOG_D("Failed to get function: %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
return (__eglMustCastToProperFunctionPointerType)proc;
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// quietly writing a differently-sized pattern.
|
||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||
if (sourceSize != elementSize) {
|
||||
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
MGLOG_W_ONCE("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
"converting between them is not implemented",
|
||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
||||
}
|
||||
@@ -1491,8 +1491,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||
// (a NULL buffers array, or a zero entry inside one).
|
||||
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||
const char* funcName) {
|
||||
if (size <= 0) {
|
||||
const char* funcName, Bool hasBuffer = true) {
|
||||
if (hasBuffer && size <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
@@ -1527,16 +1527,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
||||
// offset and the size must be multiples of 4.
|
||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
||||
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
|
||||
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
|
||||
// queryable alignment pname, which is why it was missing here), and the SIZE only for
|
||||
// transform feedback, whose capture is written in whole 32-bit components. Extending the
|
||||
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
|
||||
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
|
||||
// not required to land on four.
|
||||
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
|
||||
MG_Util::ConvertGLEnumToString(target))));
|
||||
return false;
|
||||
}
|
||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
||||
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
||||
offset, size)));
|
||||
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1548,7 +1559,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return;
|
||||
// The target's alignment rules are a property of the BINDING POINT, not of the buffer,
|
||||
// so they apply even when buffer is zero - which is exactly how
|
||||
// KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
|
||||
// (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
|
||||
// rules need a buffer, since buffer 0 detaches the binding point and ignores size.
|
||||
if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1732,10 +1748,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
||||
}
|
||||
|
||||
// ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
|
||||
// will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
|
||||
// merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
|
||||
// (KHR-GL44.multi_bind.errors_bind_buffers).
|
||||
//
|
||||
// Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
|
||||
// loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
|
||||
// call instead cost multi_bind.functional_bind_buffers_base its bindings.
|
||||
static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
|
||||
if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
|
||||
return false;
|
||||
}
|
||||
|
||||
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
||||
const GLuint buffer = buffers ? buffers[i] : 0;
|
||||
if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
|
||||
BindBufferBase_State(target, first + i, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1749,6 +1785,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLsizeiptr* sizes) {
|
||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
|
||||
if (!buffers || buffers[i] == 0) {
|
||||
BindBufferBase_State(target, first + i, 0);
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
static Bool ValidateProgramForExecution(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const char* functionName) {
|
||||
if (!currentProgram) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -34,10 +34,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
|
||||
}
|
||||
|
||||
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
|
||||
// that is the pipeline's compute stage program, not the graphics composite a draw would
|
||||
// build - which no longer contains a compute stage to find at all.
|
||||
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return false;
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -493,15 +500,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void DispatchComputeIndirect(GLintptr indirect) {
|
||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
||||
if (!dispatchComputeIndirect) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support indirect compute dispatch."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
// Argument and binding validation runs FIRST. Both are properties of the call and of GL
|
||||
// state, so a context whose backend cannot dispatch at all must still report the
|
||||
// argument error the spec names rather than masking every one of them with
|
||||
// "unsupported" - which is what put GL_INVALID_OPERATION where
|
||||
// KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
|
||||
//
|
||||
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
||||
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||
if (indirect < 0 || (indirect % 4) != 0) {
|
||||
@@ -520,6 +524,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||
return;
|
||||
}
|
||||
// ...and the same INVALID_OPERATION covers "the command would source data beyond the end
|
||||
// of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting
|
||||
// at `indirect`.
|
||||
constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32);
|
||||
if (static_cast<SizeT>(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to "
|
||||
"GL_DISPATCH_INDIRECT_BUFFER.",
|
||||
indirect, indirectBuffer->GetSize())));
|
||||
return;
|
||||
}
|
||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
||||
if (!dispatchComputeIndirect) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Backend does not support indirect compute dispatch."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -580,8 +607,80 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
// ARB_indirect_parameters / GL 4.6 core 10.4: `drawcount` is a byte offset into the buffer
|
||||
// bound to PARAMETER_BUFFER and holds one uint draw count. Three errors have to be raised
|
||||
// before the call reaches a backend, and none of them was
|
||||
// (KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount):
|
||||
// * drawcount not a multiple of four INVALID_VALUE
|
||||
// * nothing bound to PARAMETER_BUFFER, or the uint at `drawcount`
|
||||
// lies past its end INVALID_OPERATION
|
||||
// * maxdrawcount commands from `indirect` run past the end of the
|
||||
// buffer bound to DRAW_INDIRECT_BUFFER INVALID_OPERATION
|
||||
static Bool ValidateIndirectCountDraw(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride, SizeT commandSize, const char* funcName) {
|
||||
if (drawcount < 0 || (drawcount % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"drawcount must be non-negative and a multiple of four."));
|
||||
return false;
|
||||
}
|
||||
const auto& parameterBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer ||
|
||||
static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"No buffer is bound to GL_PARAMETER_BUFFER, or drawcount runs past "
|
||||
"the end of the one that is."));
|
||||
return false;
|
||||
}
|
||||
if (maxdrawcount < 0 || stride < 0 || indirect < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"indirect, maxdrawcount and stride must all be non-negative."));
|
||||
return false;
|
||||
}
|
||||
const SizeT effectiveStride = stride != 0 ? static_cast<SizeT>(stride) : commandSize;
|
||||
const auto& indirectBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
// A zero maxdrawcount sources nothing, so it cannot run past anything.
|
||||
const SizeT requiredBytes =
|
||||
maxdrawcount == 0 ? 0
|
||||
: static_cast<SizeT>(indirect) +
|
||||
static_cast<SizeT>(maxdrawcount - 1) * effectiveStride + commandSize;
|
||||
if (!indirectBuffer || requiredBytes > indirectBuffer->GetSize()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"maxdrawcount commands would be sourced from beyond the end of the "
|
||||
"buffer bound to GL_DRAW_INDIRECT_BUFFER."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
||||
// DrawElementsIndirectCommand: count, instanceCount, firstIndex, baseVertex, baseInstance.
|
||||
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
||||
5 * sizeof(Uint32), __func__)) {
|
||||
return;
|
||||
}
|
||||
// The only two draw entry points that were missing this. Every backend draw path
|
||||
// dereferences GetProgramForDraw() unconditionally, so "no current program" has to be
|
||||
// stopped here or it is a null dereference rather than the INVALID_OPERATION the spec
|
||||
// asks for - reachable through a bound pipeline that supplies no graphics stage.
|
||||
//
|
||||
// AFTER the argument checks, unlike the sibling draw entry points, and deliberately:
|
||||
// the argument rules here are properties of the call rather than of GL state, and
|
||||
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
|
||||
// they produce for a call made with no program bound. Same precedence decision, and
|
||||
// the same reason, as DispatchComputeIndirect above.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||
if (!multiDrawElementsIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -595,6 +694,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
||||
// DrawArraysIndirectCommand: count, instanceCount, first, baseInstance.
|
||||
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
||||
4 * sizeof(Uint32), __func__)) {
|
||||
return;
|
||||
}
|
||||
// See MultiDrawElementsIndirectCount, including why this one goes last.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||
if (!multiDrawArraysIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1152,7 +1259,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||
// "id is not the name of a transform feedback object" has to mean the same thing here
|
||||
// as it does to glIsTransformFeedback, and the two predicates are not interchangeable:
|
||||
// a name glGenTransformFeedbacks handed out is only reserved until it is first bound,
|
||||
// and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName
|
||||
// answers the reservation question - the right one for glBindTransformFeedback, which is
|
||||
// what turns a reserved name into an object - so using it here let a generated-but-unbound
|
||||
// name through to the completed-span check below and raised INVALID_OPERATION where the
|
||||
// spec asks for INVALID_VALUE. Name 0 is the default object and always drawable.
|
||||
if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
return (type)1; \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
@@ -1003,9 +1003,9 @@ DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenu
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
@@ -1061,7 +1061,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, G
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||
@@ -1849,7 +1849,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture,
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
|
||||
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
||||
if (!blitNamedFramebuffer) {
|
||||
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
@@ -558,7 +558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
||||
if (!clearNamedFramebufferfv) {
|
||||
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
||||
if (!clearNamedFramebufferfi) {
|
||||
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
||||
if (!clearNamedFramebufferiv) {
|
||||
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
||||
if (!clearNamedFramebufferuiv) {
|
||||
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -620,6 +620,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
||||
}
|
||||
|
||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
|
||||
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||
// The multisample TEXTURE path already resolves the limit per format
|
||||
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
|
||||
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
|
||||
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
|
||||
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
GLenum normalizedType = GL_UNSIGNED_BYTE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(normalizedInternalFormat,
|
||||
PixelFormatNormalizeOptionBit::None,
|
||||
&normalizedInternalFormat, &normalizedFormat,
|
||||
&normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, 1);
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -641,7 +669,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, const char* caller) {
|
||||
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, TextureInternalFormat format, const char* caller) {
|
||||
if (samples < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -649,9 +677,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int maxSamples = GetMaxRenderbufferSamples_State();
|
||||
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
|
||||
// glGetInternalformativ is backed; integer formats are handled below.
|
||||
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
|
||||
if (samples > maxSamples) {
|
||||
// TODO: Use per-internalformat renderbuffer sample limits once glGetInternalformativ is backed.
|
||||
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
|
||||
// INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just
|
||||
// cannot deliver it. Only a negative count is INVALID_VALUE.
|
||||
@@ -659,7 +688,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("Sample count {} exceeds GL_MAX_SAMPLES ({}).", samples, maxSamples)));
|
||||
std::format("Sample count {} exceeds this format's sample limit ({}).", samples, maxSamples)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -684,7 +713,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
||||
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, kCaller)) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, format, kCaller)) return;
|
||||
if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return;
|
||||
|
||||
renderbufferObject->AllocateStorage({width, height});
|
||||
@@ -931,7 +960,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, "NamedRenderbufferStorageMultisample_State")) return;
|
||||
if (!ValidateRenderbufferStorageSamples_State(samples, format, "NamedRenderbufferStorageMultisample_State"))
|
||||
return;
|
||||
if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return;
|
||||
|
||||
renderbufferObject->AllocateStorage({width, height});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -27,6 +28,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||
// this file's blend-state queries name unqualified.
|
||||
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||
|
||||
namespace {
|
||||
enum class IndexedBufferQueryKind {
|
||||
Binding,
|
||||
@@ -51,6 +57,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
||||
// Zero counters means zero buffers to hold them. These have to be ANSWERED rather than
|
||||
// left to the default INVALID_ENUM: a well-behaved application queries the limit exactly
|
||||
// to find out that the stage cannot do this, and an error instead both leaves its output
|
||||
// untouched (so it reads uninitialised memory and may conclude the opposite) and leaves a
|
||||
// GL error pending that surfaces at whatever unrelated call checks next.
|
||||
constexpr GLint kFrontendMaxGeometryAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
||||
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||
@@ -174,6 +189,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return frontendCount;
|
||||
}
|
||||
|
||||
// A per-stage or combined BLOCK count is an amount of indexed binding points an
|
||||
// application will occupy, and GL 4.6 table 23.64 orders the two accordingly:
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= every per-stage count,
|
||||
// and the same for the shader-storage family. The two families are answered from
|
||||
// unrelated places here - frontend constants, backend dynamic parameters, and a few
|
||||
// hard-coded TODOs - so nothing kept them ordered, and a backend that reports Vulkan
|
||||
// descriptor-indexing counts advertised 256 compute uniform blocks over 36 binding
|
||||
// points. KHR-GL44.multi_bind.dispatch_bind_buffers_base reads the block count and binds
|
||||
// that many buffers in ONE glBindBuffersBase, which is then INVALID_OPERATION before it
|
||||
// binds anything. Clamping is the only direction available: the binding count is the
|
||||
// capacity of the state layer's indexed-binding array, not a number we may inflate.
|
||||
GLint ClampBlockCountToBindingPoints(GLint blockCount, BufferTarget bufferTarget) {
|
||||
const GLint bindingPoints = static_cast<GLint>(GetIndexedBufferQueryPointCount(bufferTarget));
|
||||
return std::min(std::max(blockCount, 0), bindingPoints);
|
||||
}
|
||||
|
||||
GLint ClampUniformBlockCount(GLint blockCount) {
|
||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::Uniform);
|
||||
}
|
||||
|
||||
GLint ClampStorageBlockCount(GLint blockCount) {
|
||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
||||
}
|
||||
|
||||
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
||||
if (pname == GL_DRAW_BUFFER) {
|
||||
drawBufferIndex = 0;
|
||||
@@ -306,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||
// index with viewport 0's value, which is what
|
||||
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||
// getter width funnels through this so they can never disagree with each other.
|
||||
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||
switch (target) {
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
out[0] = viewport.x();
|
||||
out[1] = viewport.y();
|
||||
out[2] = viewport.z();
|
||||
out[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_SCISSOR_BOX: {
|
||||
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||
out[0] = static_cast<GLfloat>(box.x());
|
||||
out[1] = static_cast<GLfloat>(box.y());
|
||||
out[2] = static_cast<GLfloat>(box.z());
|
||||
out[3] = static_cast<GLfloat>(box.w());
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_RANGE: {
|
||||
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
out[0] = range.x();
|
||||
out[1] = range.y();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||
static_cast<Uint32>(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -350,7 +433,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
|
||||
@@ -409,7 +492,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -596,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||
params[0] = viewport.x();
|
||||
params[1] = viewport.y();
|
||||
params[2] = viewport.z();
|
||||
params[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
@@ -722,10 +816,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core table 23.4/23.5: *_BUFFER_SIZE reports the size glBindBufferRange
|
||||
// was ASKED for, verbatim. It is not clamped to the buffer's storage, and it does
|
||||
// not follow the buffer when a later glBufferData resizes it - a range may legally
|
||||
// name bytes the buffer does not have yet. Clamping it here answered 0 for the
|
||||
// common conformance shape of binding a range on a buffer that has no storage
|
||||
// yet (KHR-GL43.shader_storage_buffer_object.basic-binding).
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
||||
*data = static_cast<GLint>(end - start);
|
||||
*data = static_cast<GLint>(range.end - range.start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
@@ -755,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi(target, index);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_DEPTH_RANGE: {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
@@ -890,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||
ReadIndexedViewportStateFloat(target, index, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -907,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLdouble>(values[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -951,9 +1074,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
||||
*data = static_cast<GLint64>(end - start);
|
||||
// Verbatim, unclamped - see the GetIntegeri_v arm.
|
||||
*data = static_cast<GLint64>(range.end - range.start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
@@ -961,15 +1083,35 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
auto getInteger64i = MG_Backend::gBackendFunctionsTable.GL.GetInteger64i_v;
|
||||
if (!getInteger64i) {
|
||||
*data = 0;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
|
||||
// The one indexed pname whose value genuinely needs 64 bits: a vertex buffer binding
|
||||
// offset is an intptr, so taking the 32-bit route below would truncate it.
|
||||
if (target == GL_VERTEX_BINDING_OFFSET) {
|
||||
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Vertex buffer binding index is out of range."));
|
||||
return;
|
||||
}
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
*data = vao ? static_cast<GLint64>(vao->GetBindingPoint(index).Offset) : 0;
|
||||
return;
|
||||
}
|
||||
getInteger64i(target, index, data);
|
||||
|
||||
// Everything else is 32-bit indexed state that the glGetIntegeri_v pname table already
|
||||
// owns, and GL 4.6 core 22.1 says every indexed query answers every indexed pname.
|
||||
// Handing the leftovers straight to the backend instead made glGetInteger64i_v disagree
|
||||
// with glGetIntegeri_v on the very same pname - GL_MAX_COMPUTE_WORK_GROUP_COUNT read
|
||||
// back 0 while the 32-bit view said 65535 (KHR-GL43.compute_shader.max), because a
|
||||
// frontend-only value simply is not in the driver's table.
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLint64>(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||
@@ -1379,7 +1521,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxCombinedAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxCombinedUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
|
||||
*params = 1; // TODO
|
||||
@@ -1394,7 +1536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxFragmentInputComponents;
|
||||
@@ -1411,13 +1553,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentUniformVectors;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxFragmentUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryInputComponents;
|
||||
@@ -1440,7 +1585,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxGeometryUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryUniformComponents;
|
||||
@@ -1472,9 +1617,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessControlAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
return;
|
||||
@@ -1482,10 +1633,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
@@ -1502,13 +1653,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
|
||||
*params = kFrontendMaxVertexAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
|
||||
*params = kFrontendMaxVertexAtomicCounterBuffers;
|
||||
return;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||
return;
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
*params = ClampStorageBlockCount(16); // TODO
|
||||
return;
|
||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxVertexUniformComponents;
|
||||
@@ -1520,7 +1674,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxVertexOutputComponents;
|
||||
return;
|
||||
case GL_MAX_VERTEX_UNIFORM_BLOCKS:
|
||||
*params = kFrontendMaxVertexUniformBlocks;
|
||||
*params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks);
|
||||
return;
|
||||
case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
|
||||
*params = 0; // compressed texture upload entrypoints are still unimplemented
|
||||
@@ -1891,7 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -1920,13 +2074,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
|
||||
*params = dynamicParameters.MaxComputeShaderStorageBlocks;
|
||||
*params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks);
|
||||
break;
|
||||
case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
|
||||
*params = dynamicParameters.MaxCombinedShaderStorageBlocks;
|
||||
*params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
|
||||
*params = dynamicParameters.MaxComputeUniformBlocks;
|
||||
*params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
|
||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
@@ -2056,7 +2210,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||
break;
|
||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||
// The conformance suite splits this evenly across every advertised binding point and
|
||||
// binds all of them in one glBindBuffersRange
|
||||
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
|
||||
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
|
||||
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
||||
// one counter per binding point.
|
||||
*params = std::max<GLint>(
|
||||
kFrontendMaxAtomicCounterBufferSize,
|
||||
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
||||
break;
|
||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxTextureBufferSize;
|
||||
@@ -2121,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.MaxViewportHeight;
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
*params = dynamicParameters.MaxViewports;
|
||||
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||
// carrying that device number for exactly that decision.
|
||||
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||
@@ -2177,7 +2347,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
||||
std::format("Invalid enum: 0x{:X}", pname)));
|
||||
|
||||
@@ -744,6 +744,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||
}
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while the compile job is still in flight -
|
||||
// and, via the latch below, for the rest of that node's life once any query was
|
||||
// answered this way - GL_COMPILE_STATUS reads GL_TRUE and the info log reads empty,
|
||||
// WITHOUT joining. The latch (TakeOptimisticCompileAnswer) is what makes the three
|
||||
// sites tell ONE story: without it, a job settling between an application's info-log
|
||||
// read and its status read would produce the torn pair "GL_FALSE with an empty log",
|
||||
// and an application that aborts on that never reaches the link join that carries the
|
||||
// real diagnostic. A failure hidden here still fails the program link, with the
|
||||
// compile log quoted in the program info log (ProgramLinkTask::ConsumeShaders), which
|
||||
// is where the serial compile-then-check applications this exists for do their error
|
||||
// handling.
|
||||
static Bool AnswerCompileOptimistically(const SharedPtr<MG_State::GLState::ShaderObject>& shaderObject) {
|
||||
return MG_Util::Async::OptimisticShaderStatusActive() && shaderObject->TakeOptimisticCompileAnswer();
|
||||
}
|
||||
|
||||
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
@@ -756,9 +771,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = shaderObject->GetDeleteStatus();
|
||||
break;
|
||||
case GL_COMPILE_STATUS:
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
*params = GL_TRUE;
|
||||
break;
|
||||
}
|
||||
*params = shaderObject->GetCompileStatus();
|
||||
break;
|
||||
case GL_INFO_LOG_LENGTH:
|
||||
// Not cosmetic: LWJGL's one-argument glGetShaderInfoLog convenience overload
|
||||
// sizes its buffer from this query, so a joining answer here would defeat the
|
||||
// non-joining GetShaderInfoLog below.
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
*params = 0;
|
||||
break;
|
||||
}
|
||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||
break;
|
||||
case GL_SHADER_SOURCE_LENGTH:
|
||||
@@ -784,6 +810,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
|
||||
// See AnswerCompileOptimistically: an in-flight compile reads as an empty log. The
|
||||
// cost is a lost compile WARNING (a successful compile whose log the application
|
||||
// reads exactly once, now, and never after the join) - accepted as part of the
|
||||
// opt-in.
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
CopyStr(bufSize, length, infoLog, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& log = shaderObject->GetInfoLog();
|
||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||
}
|
||||
@@ -815,7 +850,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// vector per column - while the value glGetUniform* must return is tightly packed
|
||||
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
|
||||
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
|
||||
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
|
||||
// there. Returns false when there is nothing here to unpack.
|
||||
//
|
||||
// A DOUBLE matrix is declined not because it is laid out differently - it is not, the
|
||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||
// each float back to the queried type, and it undoes the same padding itself.
|
||||
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||
const Int columns = ttype->getMatrixCols();
|
||||
@@ -828,12 +868,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||
// everything except a float matrix, whose padded columns make it wider.
|
||||
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
||||
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
||||
// one and two copies of a layout rule is one too many.
|
||||
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||
if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) {
|
||||
return static_cast<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
|
||||
}
|
||||
return tightSize;
|
||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||
}
|
||||
|
||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||
@@ -869,13 +908,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||
// which for a `double` uniform is twice its storage - every 64-bit float is
|
||||
// narrowed before the module reaches a backend, so the slot holds floats. The
|
||||
// typed entry points (glGetUniformdv and friends) go through
|
||||
// GetUniformScalar_State, which converts component by component; this raw
|
||||
// copy has no type to convert with, so it is bounded rather than converted.
|
||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||
}
|
||||
}
|
||||
// TODO: handle 1i variant as texture unit
|
||||
@@ -917,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
@@ -926,22 +971,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||
}
|
||||
|
||||
// A double-precision uniform is the one case where the stored component type can
|
||||
// differ from the queried one for a non-opaque uniform, and the difference is not
|
||||
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
|
||||
// caller's buffer as well as return nonsense. Read component by component and let
|
||||
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
|
||||
// A double-precision uniform is the one case where the stored component type differs
|
||||
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||
// narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||
// two components reinterpreted as one. Read component by component and let GL's
|
||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||
// widens back to the queried type, having lost precision at the glUniform*d that
|
||||
// stored it and not here.
|
||||
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||
// The slot the linker handed out is exactly `columns` columns wide, so it also
|
||||
// states the column stride - which for a double matrix is not a float's 16 bytes.
|
||||
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
|
||||
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||
// tightly packed run and never reaches the stride at all.
|
||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||
for (Int column = 0; column < columns; ++column) {
|
||||
for (Int row = 0; row < rows; ++row) {
|
||||
GLdouble component = 0.0;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
|
||||
GLfloat component = 0.0f;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||
sizeof(component));
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
// Rounded to the nearest integer and clamped into the queried type's
|
||||
@@ -1012,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!initialized) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -1085,23 +1135,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, programObject.GetMaxUniformLocation());
|
||||
// Record the write for the pipeline composite's uniform mirror, which copies only
|
||||
// the locations a stage program has actually been written to (see
|
||||
// ProgramObject::MarkUniformWrittenAtLocation). Here rather than further down
|
||||
// because every exit below is still a write as far as GL is concerned: the
|
||||
// buffered-write detour returns early, the bytes-equal dedupe returns early, and
|
||||
// even the no-backing-storage bail is a uniform the application addressed. This is
|
||||
// the funnel EVERY glUniform* and glProgramUniform* entry point reaches, once per
|
||||
// LOCATION - so an array element write marks that element and nothing else. On a
|
||||
// program that can never be a pipeline stage - the monolithic glUseProgram path,
|
||||
// which is where the thousands of calls per frame are - this is one bool branch.
|
||||
programObject.MarkUniformWrittenAtLocation(location);
|
||||
// Everything up to and including the clamp is phase-A data (the uniform's GL type
|
||||
// decides its size), so it is answered without joining anything.
|
||||
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
||||
const Uint offset = programObject.GetUniformOffset(location);
|
||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||
const SizeT uboSize = programObject.GetUBOSize();
|
||||
SizeT writeSize = ItemCount * sizeof(T);
|
||||
if (size < writeSize) {
|
||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
"bytes; clamping",
|
||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||
writeSize = size;
|
||||
}
|
||||
// The uniform shadow's LAYOUT is phase-B data, so a write that lands while the
|
||||
// SPIR-V job is still running is recorded and replayed at its publish instead of
|
||||
// joining it. This is the hot path for a shaderpack that sets its uniforms
|
||||
// immediately after glLinkProgram. BufferUniformWrite declines (and we fall
|
||||
// through, joining) only past its size budget.
|
||||
if (programObject.IsSpirvPending() &&
|
||||
programObject.BufferUniformWrite(location, byteOffsetInsideUniform, value, writeSize)) {
|
||||
return;
|
||||
}
|
||||
const Uint offset = programObject.GetUniformOffset(location);
|
||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||
const SizeT uboSize = programObject.GetUBOSize();
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||
// Should not happen: linking gives every settable uniform backing
|
||||
// storage. Log and drop the write instead of faulting.
|
||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
"uboSize=%zu); dropping write",
|
||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||
writeSize, uboSize);
|
||||
@@ -1192,36 +1264,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
|
||||
// upload template - it is already typed on the component - but a matrix does: the
|
||||
// column stride the linker used for a double matrix is not the 16 bytes a float one
|
||||
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
|
||||
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
|
||||
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLdouble> column(static_cast<SizeT>(rows));
|
||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
|
||||
return;
|
||||
}
|
||||
const GLdouble* source = value + matrix * componentCount;
|
||||
for (Int c = 0; c < columns; ++c) {
|
||||
for (Int r = 0; r < rows; ++r) {
|
||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||
}
|
||||
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
|
||||
for (Int r = 1; r < rows; ++r) {
|
||||
Uniform_State<1>(programObject, location + matrix, column.data() + r,
|
||||
c * columnStride + r * sizeof(GLdouble));
|
||||
}
|
||||
}
|
||||
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||
//
|
||||
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||
template <GLsizei ItemCount>
|
||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
// Same shape as the float entry points: the location validation still runs, and a
|
||||
// null pointer is left to fault exactly where glUniform*fv would.
|
||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||
}
|
||||
|
||||
template <GLsizei ItemCount>
|
||||
void ProgramUniformvNarrowed_State(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||
}
|
||||
|
||||
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
|
||||
@@ -1270,6 +1345,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
||||
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
||||
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
||||
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
||||
// is then the one implementation both spellings share.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
if (value == nullptr || count <= 0) return;
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
UniformMatrixfv_Object(programObject, "glUniformMatrixdv", location, count, transpose, narrowed.data(),
|
||||
columns, rows, "the current program object");
|
||||
}
|
||||
|
||||
// Helper function to transpose a 2x2 matrix
|
||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||
// Input matrix is in column-major order (OpenGL default)
|
||||
@@ -1716,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
@@ -2033,71 +2124,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void Uniform1d(GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
Uniformv_State<1>(location, 1, v);
|
||||
UniformvNarrowed_State<1>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<1>(location, count, value);
|
||||
UniformvNarrowed_State<1>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
|
||||
const GLdouble v[] = {v0};
|
||||
ProgramUniformv_State<1>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<1>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<1>(program, location, count, value);
|
||||
}
|
||||
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
Uniformv_State<2>(location, 1, v);
|
||||
UniformvNarrowed_State<2>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<2>(location, count, value);
|
||||
UniformvNarrowed_State<2>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
|
||||
const GLdouble v[] = {v0, v1};
|
||||
ProgramUniformv_State<2>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<2>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<2>(program, location, count, value);
|
||||
}
|
||||
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
Uniformv_State<3>(location, 1, v);
|
||||
UniformvNarrowed_State<3>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<3>(location, count, value);
|
||||
UniformvNarrowed_State<3>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||
const GLdouble v[] = {v0, v1, v2};
|
||||
ProgramUniformv_State<3>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<3>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<3>(program, location, count, value);
|
||||
}
|
||||
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
Uniformv_State<4>(location, 1, v);
|
||||
UniformvNarrowed_State<4>(location, 1, v);
|
||||
}
|
||||
|
||||
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||
Uniformv_State<4>(location, count, value);
|
||||
UniformvNarrowed_State<4>(location, count, value);
|
||||
}
|
||||
|
||||
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||
const GLdouble v[] = {v0, v1, v2, v3};
|
||||
ProgramUniformv_State<4>(program, location, 1, v);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, 1, v);
|
||||
}
|
||||
|
||||
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||
ProgramUniformv_State<4>(program, location, count, value);
|
||||
ProgramUniformvNarrowed_State<4>(program, location, count, value);
|
||||
}
|
||||
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
@@ -2658,6 +2749,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||
// Every early-out below reports "nothing was written", and it has to say so before it can
|
||||
// take one: callers legitimately leave *length uninitialised and then loop to it. The CTS
|
||||
// does exactly that (gl4cProgramInterfaceQueryTests.cpp:2172 declares `GLsizei length;` and
|
||||
// walks `for (i = 0; i < length; ++i)` over a 1000-entry stack array), so an untouched
|
||||
// *length turned every error path here into a stack overrun inside the caller -
|
||||
// KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 entries and died on
|
||||
// both backends. The success path overwrites this with the real count.
|
||||
if (length) *length = 0;
|
||||
|
||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||
if (!programObject) return;
|
||||
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||
|
||||
@@ -19,16 +19,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
|
||||
}
|
||||
|
||||
// A pipeline name only names an object once it has been bound or created; querying a
|
||||
// reserved-but-unmaterialised name is INVALID_OPERATION (GL 4.6 core 7.4).
|
||||
// GL 4.6 core 7.4 asks only that the name came from GenProgramPipelines and has not been
|
||||
// deleted - so a name that was reserved and never bound is legal here, and the command
|
||||
// MATERIALIZES it rather than rejecting it.
|
||||
//
|
||||
// Requiring a bound object instead is what broke every separable-program conformance case
|
||||
// across three families: the CTS reserves a name, calls glUseProgramStages three times and
|
||||
// only then binds, which is the order the spec's own example uses. Each of those calls
|
||||
// failed with INVALID_OPERATION, so the stage programs were never recorded - the pipeline
|
||||
// stayed empty, GetProgramForDraw flattened nothing and the draw painted nothing, and the
|
||||
// rejected calls' error was left in the queue for the harness to find. One cause, both
|
||||
// symptoms.
|
||||
const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
|
||||
const char* function) {
|
||||
if (!MG_State::pGLContext->IsProgramPipelineObject(pipeline)) {
|
||||
const auto& object = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
|
||||
if (!object) {
|
||||
RecordPipelineError(ErrorCode::InvalidOperation, function,
|
||||
std::format("Program pipeline {} does not exist.", pipeline));
|
||||
return nullptr;
|
||||
}
|
||||
return &MG_State::pGLContext->GetProgramPipelineObject(pipeline);
|
||||
return &object;
|
||||
}
|
||||
|
||||
Bool ValidatePipelineCount(GLsizei n, const char* function) {
|
||||
|
||||
@@ -210,11 +210,66 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
|
||||
// ---- model construction --------------------------------------------------------
|
||||
|
||||
// GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
|
||||
//
|
||||
// glslang records a block reference by walking up to the base symbol and calling
|
||||
// addBlockName with the whole ARRAY type, which ORs the referencing stage into every
|
||||
// element at once - it has not resolved the subscript yet at that point. So reading
|
||||
// "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
|
||||
// stage (KHR-GL43.program_interface_query.uniform-block-types).
|
||||
//
|
||||
// The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
|
||||
// every element with the stage mask suppressed, and only the dereference chain actually
|
||||
// walked turns a bit on - and that chain carries the subscript. So the union of a block
|
||||
// instance's members is the reference set of that instance.
|
||||
//
|
||||
// Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
|
||||
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
||||
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
||||
// back to the block's own mask there would restore the over-approximation.
|
||||
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
||||
for (Int index = 0; index < uniformCount; ++index) {
|
||||
const auto& uniform = mutableReflection.getUniform(index);
|
||||
const Int owner = uniform.index;
|
||||
if (owner < 0 || owner >= blockCount) continue;
|
||||
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||
}
|
||||
return stagesByBlock;
|
||||
}
|
||||
|
||||
// UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
|
||||
// names glslang produces for a uniform block array carry the subscript
|
||||
// ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
|
||||
// distinct entries and the bits land on the right one. A SHADER STORAGE block array does
|
||||
// NOT get that treatment - its buffer variables reflect under one subscript-free spelling
|
||||
// shared by every element - so a union over them credits element 0 and starves the rest.
|
||||
// KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
|
||||
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
||||
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
||||
// blocks therefore keep that mask untouched.
|
||||
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||
Int tIndex) {
|
||||
String arrayBase;
|
||||
Uint element = 0;
|
||||
Bool malformed = false;
|
||||
if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
|
||||
return static_cast<Uint32>(block.stages);
|
||||
}
|
||||
if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
|
||||
return static_cast<Uint32>(block.stages);
|
||||
}
|
||||
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||
}
|
||||
|
||||
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||
blockInterfaceIndex.assign(blockCount, -1);
|
||||
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||
|
||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||
@@ -260,8 +315,8 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||
if (tIndex >= 0 && tIndex < blockCount) {
|
||||
resource.stages =
|
||||
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
|
||||
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
||||
stagesFromMembers, tIndex);
|
||||
}
|
||||
model.uniformBlocks.push_back(Move(resource));
|
||||
}
|
||||
@@ -352,6 +407,17 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// A built-in interface block that a shader redeclares with fewer members keeps the
|
||||
// omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
|
||||
// (basic type void) instead of erasing them, because the original shared declaration
|
||||
// has to stay usable. Only the instance-named form erases. So a separable vertex
|
||||
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||
// are not part of its output interface.
|
||||
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
||||
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
||||
}
|
||||
|
||||
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||
|
||||
@@ -359,6 +425,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
for (Int index = 0; index < inputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeInput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
if (IsHiddenBlockMember(type)) continue;
|
||||
Resource resource;
|
||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||
// names; GL enumerates the GL spellings.
|
||||
@@ -373,18 +440,28 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||
model.programInputs.push_back(Move(resource));
|
||||
}
|
||||
|
||||
// A color number, and therefore a color INDEX, exists only for a fragment stage's
|
||||
// outputs. The output interface belongs to the program's last stage, so for a
|
||||
// separable tessellation/geometry/vertex program these are varyings: asking the
|
||||
// frag-data maps about them can still answer a location (a tess-control output
|
||||
// carries its own layout(location=N)), and a location then manufactures a color
|
||||
// index of 0 where GL requires -1
|
||||
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
||||
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||
for (Int index = 0; index < outputCount; ++index) {
|
||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||
const glslang::TType* type = refl.getType();
|
||||
if (IsHiddenBlockMember(type)) continue;
|
||||
Resource resource;
|
||||
resource.name = WithArraySuffix(refl.name, type);
|
||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||
if (resource.location < 0) {
|
||||
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
|
||||
// output both have no location, and therefore no color index either.
|
||||
if (resource.location < 0 || !lastStageIsFragment) {
|
||||
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||
// non-fragment stage's outputs have no color number at all - either way there
|
||||
// is no color index.
|
||||
resource.locationIndex = -1;
|
||||
} else {
|
||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||
|
||||
@@ -344,6 +344,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyAllQueryObjects() {
|
||||
// Detach the registry under the lock and release it outside. Entries the app
|
||||
// already deleted were erased by DeleteQueries, so nothing here double-frees;
|
||||
// a DeleteQueries racing this sweep finds an empty registry and ignores the
|
||||
// names. The active-query slots and the name allocator are reset under the
|
||||
// same lock: query names are context-owned state, so a fresh context must
|
||||
// start clean instead of inheriting the dead context's allocator cursor or
|
||||
// a stale "a query is already active on this target" latch.
|
||||
UnorderedMap<GLuint, QueryObject*> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
orphans.swap(g_liveQueryObjects);
|
||||
g_nextQueryId = 1;
|
||||
g_activeTimeElapsedQueryId = 0;
|
||||
g_activePrimitivesWrittenQueryId = 0;
|
||||
g_activePrimitivesGeneratedQueryId = 0;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
}
|
||||
if (orphans.empty()) {
|
||||
return;
|
||||
}
|
||||
// Both backends' DeleteBackendQuery only free the heap wrapper once their GL
|
||||
// context/renderer is gone (generation/current-thread guards), so this is
|
||||
// safe after the backend has released its EGL resources - but not after the
|
||||
// function table itself is cleared.
|
||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
|
||||
GLboolean IsQuery(GLuint id) {
|
||||
if (id == 0) {
|
||||
return GL_FALSE;
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GenQueries(GLsizei n, GLuint* ids);
|
||||
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||
void DeleteQueries(GLsizei n, const GLuint* ids);
|
||||
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||
// Query objects are context-owned, and MobileGL::Destroy() tears every context
|
||||
// down, so the process-global registry has to be drained there: without this the
|
||||
// QueryObject and any backend timer-query wrapper leaked across every
|
||||
// eglTerminate/eglInitialize cycle, and the active-query/name-allocator state
|
||||
// from the dead context survived into the next one. Must run while the backend
|
||||
// function table is still populated, and before a re-initialized library could
|
||||
// pair the handles with the wrong backend's DeleteBackendQuery.
|
||||
void DestroyAllQueryObjects();
|
||||
GLboolean IsQuery(GLuint id);
|
||||
void BeginQuery(GLenum target, GLuint id);
|
||||
void EndQuery(GLenum target);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "GL_RenderState.h"
|
||||
#include <cmath>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
@@ -19,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
if (target != GL_BLEND) {
|
||||
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
GLuint limit = 0;
|
||||
const char* indexName = nullptr;
|
||||
switch (target) {
|
||||
case GL_BLEND:
|
||||
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||
indexName = "Buffer";
|
||||
break;
|
||||
case GL_SCISSOR_TEST:
|
||||
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||
indexName = "Viewport";
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Only GL_BLEND is supported for indexed capability state."));
|
||||
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||
"capability state."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||
if (index >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(indexName) + " index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||
"."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Viewport index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||
return false;
|
||||
}
|
||||
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"first (" + std::to_string(first) + ") + count (" +
|
||||
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||
if (width >= T(0) && height >= T(0)) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||
// error queue holds exactly one entry.
|
||||
template <typename T>
|
||||
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||
")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||
if (v != nullptr) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||
::MobileGL::BlendEquation& outEquation) {
|
||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||
@@ -92,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array setters ------------------
|
||||
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||
}
|
||||
|
||||
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||
}
|
||||
|
||||
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
first + static_cast<GLuint>(i),
|
||||
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||
}
|
||||
}
|
||||
|
||||
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||
}
|
||||
|
||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
Bool applyFront = false;
|
||||
Bool applyBack = false;
|
||||
@@ -174,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||
}
|
||||
@@ -335,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||
return GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -380,7 +520,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
*data = IsEnabledi_State(target, index);
|
||||
// GL 4.6 core 22.1: glGetBooleani_v answers EVERY indexed state, not just the indexed
|
||||
// capabilities - a non-boolean value simply reads back as "is it non-zero". Routing the
|
||||
// non-capability enums to the pname table glGetIntegeri_v already owns is what makes
|
||||
// that true; without it a query like glGetBooleani_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0)
|
||||
// came back GL_INVALID_ENUM (KHR-GL43.compute_shader.max).
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi_State(target, index);
|
||||
return;
|
||||
}
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||
? 4
|
||||
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
GLboolean IsEnabled_State(GLenum cap) {
|
||||
@@ -713,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Disablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -731,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Enablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -785,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Viewport_State(x, y, width, height);
|
||||
}
|
||||
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
ViewportArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
ViewportIndexedf_State(index, x, y, w, h);
|
||||
}
|
||||
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||
ScissorArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
ScissorIndexed_State(index, left, bottom, width, height);
|
||||
}
|
||||
|
||||
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
DepthRangeArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||
DepthRangeIndexed_State(index, n, f);
|
||||
}
|
||||
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Enablei(GLenum target, GLuint index);
|
||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GL_Sampler.h"
|
||||
#include "Validators.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
#include "../Texture/GL_Texture.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -269,15 +270,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
|
||||
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
|
||||
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
|
||||
// matter what the driver reports.
|
||||
// The number of texture units a sampler may be bound to is the same count a TEXTURE may be
|
||||
// bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
|
||||
// computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
|
||||
// readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
|
||||
// come to disagree about which units exist.
|
||||
static GLint GetSamplerBindableTextureUnitCount() {
|
||||
GLint maxTextureUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
return GetCombinedTextureImageUnitCount();
|
||||
}
|
||||
|
||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||
@@ -336,8 +335,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// ARB_multi_bind adds one rule the single-bind path does not have: "samplers will not be
|
||||
// created if they do not exist", so a name that is not an existing sampler OBJECT is
|
||||
// INVALID_OPERATION here (KHR-GL44.multi_bind.errors_bind_samplers). Per element, not
|
||||
// all-or-nothing - the extension defines glBindSamplers as a loop, so a bad entry costs
|
||||
// its own texture unit and leaves the rest of the range bound.
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
||||
const GLuint sampler = samplers ? samplers[i] : 0;
|
||||
if (sampler != 0 && !MG_State::pGLContext->ValidateSamplerObject(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "BindSamplers",
|
||||
std::format("samplers[{}] ({}) is not the name of an existing sampler object.", i, sampler)));
|
||||
continue;
|
||||
}
|
||||
BindSampler_State(first + i, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Frontend sync object: wraps an optional backend fence handle. A null
|
||||
// backend handle (backend has no fence support, or could not create a
|
||||
// fence at call time) keeps the legacy always-signaled behavior.
|
||||
//
|
||||
// SharedPtr-owned, not raw: DeleteSync can remove the registry entry while
|
||||
// another thread is inside ClientWaitSync/GetSynciv. Those callers hold a
|
||||
// SharedPtr copy, so the object stays alive until the last reader leaves.
|
||||
// `mutex` then serializes backend-handle reads against the one-time
|
||||
// backend-handle release performed by DeleteSync / DestroyAllSyncObjects.
|
||||
struct SyncObject {
|
||||
std::mutex mutex;
|
||||
MG_Backend::BackendSyncHandle backendHandle = nullptr;
|
||||
GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE;
|
||||
GLbitfield flags = 0;
|
||||
|
||||
void ReleaseBackendHandle() {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
if (backendHandle == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync) {
|
||||
backendDeleteSync(backendHandle);
|
||||
}
|
||||
backendHandle = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync calls may arrive from any thread (launchers migrate the context
|
||||
@@ -25,9 +44,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Entries left at process shutdown are simply dropped; their backend
|
||||
// handles die with the backend.
|
||||
std::mutex g_syncObjectsMutex;
|
||||
UnorderedMap<GLsync, SyncObject*> g_liveSyncObjects;
|
||||
UnorderedMap<GLsync, SharedPtr<SyncObject>> g_liveSyncObjects;
|
||||
|
||||
SyncObject* FindSyncObject(GLsync sync) {
|
||||
SharedPtr<SyncObject> FindSyncObject(GLsync sync) {
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
return it != g_liveSyncObjects.end() ? it->second : nullptr;
|
||||
@@ -35,13 +54,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} // namespace
|
||||
|
||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||
auto* syncObject = new SyncObject;
|
||||
auto syncObject = MakeShared<SyncObject>();
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject.get());
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
g_liveSyncObjects[handle] = syncObject;
|
||||
return handle;
|
||||
@@ -52,24 +71,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return GL_WAIT_FAILED;
|
||||
}
|
||||
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
// Hold the per-object lock across the backend call: a concurrent
|
||||
// DeleteSync may already have removed this object from the registry, but
|
||||
// it cannot free the backend handle (or the wrapper) until this reader
|
||||
// finishes. ClientWaitSync can block for `timeout`; that blocks only this
|
||||
// sync object, never the registry or unrelated syncs.
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
if (!backendClientWaitSync || syncObject->backendHandle == nullptr) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
return;
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
if (backendWaitSync && syncObject->backendHandle != nullptr) {
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +104,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sync == nullptr) {
|
||||
return; // glDeleteSync(0) is silently ignored
|
||||
}
|
||||
SyncObject* syncObject = nullptr;
|
||||
SharedPtr<SyncObject> syncObject;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
const auto it = g_liveSyncObjects.find(sync);
|
||||
@@ -88,15 +114,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
syncObject = it->second;
|
||||
g_liveSyncObjects.erase(it);
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
// Release the backend handle under the object lock. The local SharedPtr
|
||||
// (and any reader's SharedPtr) keeps the wrapper itself alive until every
|
||||
// in-flight backend call has returned.
|
||||
syncObject->ReleaseBackendHandle();
|
||||
}
|
||||
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
|
||||
const auto* syncObject = FindSyncObject(sync);
|
||||
const SharedPtr<SyncObject> syncObject = FindSyncObject(sync);
|
||||
if (!syncObject) {
|
||||
if (length) {
|
||||
*length = 0;
|
||||
@@ -111,7 +136,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
const std::lock_guard<std::mutex> lock(syncObject->mutex);
|
||||
const Bool signaled = !backendGetSyncStatus || syncObject->backendHandle == nullptr ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
break;
|
||||
@@ -137,11 +163,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DestroyAllSyncObjects() {
|
||||
// Detach the registry under the lock, release outside it. Entries the app
|
||||
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
||||
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
||||
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
||||
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
||||
// race an app-driven DeleteSync already has.
|
||||
UnorderedMap<GLsync, SyncObject*> orphans;
|
||||
// a DeleteSync racing this sweep finds an empty registry and returns.
|
||||
// Readers racing this sweep keep their SharedPtr copy alive, and each
|
||||
// object's own lock makes the backend-handle release wait for them.
|
||||
UnorderedMap<GLsync, SharedPtr<SyncObject>> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
orphans.swap(g_liveSyncObjects);
|
||||
@@ -153,12 +178,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// context/renderer is gone (generation/current-thread guards), so this is
|
||||
// safe after the backend has released its EGL resources - but not after
|
||||
// the function table itself is cleared.
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
if (syncObject) {
|
||||
syncObject->ReleaseBackendHandle();
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
|
||||
@@ -613,6 +613,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Compressed texture formats are not supported."));
|
||||
}
|
||||
|
||||
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
|
||||
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
|
||||
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
|
||||
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
|
||||
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
||||
// above for the compressed-format path.
|
||||
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
||||
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
||||
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
||||
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"Level queries are not supported for texture-buffer storage."));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||
@@ -853,7 +870,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (readBytesPerTexel != bytesPerTexel) {
|
||||
MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
bytesPerTexel, readBytesPerTexel);
|
||||
return false;
|
||||
}
|
||||
@@ -1359,6 +1376,47 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The same rules for the COMPRESSED entry points, whose payload size is the imageSize the
|
||||
// caller passed rather than something derived from a (format, type) pair - and which have no
|
||||
// datum size, so the alignment rule above does not apply to them. Shared by
|
||||
// glCompressedTexImage2D and glCompressedTexSubImage2D so the two cannot drift; the point
|
||||
// that is easy to get wrong and that KHR-GL44.buffer_storage.map_persistent_texture exists to
|
||||
// check is the first one: a PERSISTENT mapping stays a legal transfer source.
|
||||
Bool ValidateCompressedUnpackBufferSource(const void* data, SizeT imageSize, const char* caller) {
|
||||
const auto& unpackBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (!unpackBuffer) return true;
|
||||
|
||||
if (unpackBuffer->IsMapped() && !(unpackBuffer->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel unpack buffer is currently mapped."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT offset = reinterpret_cast<SizeT>(data);
|
||||
const SizeT bufferSize = unpackBuffer->GetSize();
|
||||
if (offset > bufferSize || imageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Unpacking would read past the end of the pixel unpack buffer."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Where a compressed upload reads its blocks from: `data` is an offset into the bound unpack
|
||||
// buffer when there is one, and a client pointer otherwise. Only meaningful once
|
||||
// ValidateCompressedUnpackBufferSource has passed. Null means there is nothing to read, which
|
||||
// GL leaves undefined and which callers must not dereference.
|
||||
const void* CompressedUnpackSource(const void* data) {
|
||||
const auto& unpackBuffer =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (!unpackBuffer) return data;
|
||||
return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
|
||||
}
|
||||
|
||||
void TexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
@@ -1432,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
||||
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -1541,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
{width, height, 1}, false, inputSize);
|
||||
|
||||
if (!processedPixels || inputSize == 0) {
|
||||
MGLOG_E("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
height);
|
||||
if (processedPixels) free(processedPixels);
|
||||
return;
|
||||
@@ -1555,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
|
||||
MGLOG_E("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -2106,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
__func__, imageSize, internalBytes);
|
||||
}
|
||||
@@ -2221,6 +2279,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{width, height, 1}, internalBytes});
|
||||
// GL 4.6 core 8.5: a SPECIFIC compressed internalformat (unlike a generic
|
||||
// GL_COMPRESSED_* one, where the implementation is free to choose) commits the
|
||||
// level to that format - GL_TEXTURE_COMPRESSED must then answer true for it and
|
||||
// GL_TEXTURE_INTERNAL_FORMAT must report it, which is how an application asks for
|
||||
// the size to hand glCompressedTexSubImage2D afterwards. Only the tag and the size
|
||||
// are recorded: there is no BC/ETC codec here, so the texel shadow keeps the
|
||||
// uncompressed storage this format resolved to (which is also what lets the level
|
||||
// sample as the application's texels), and the compressed image the tag describes
|
||||
// is zero-filled - the one reproducible answer glGetCompressedTexImage can give for
|
||||
// an image nothing ever compressed. AllocateStorage above clears the tag, so this
|
||||
// has to follow it.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
@@ -2235,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
imageSize, internalBytes);
|
||||
}
|
||||
@@ -2910,7 +2985,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -2924,7 +3000,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -2938,16 +3015,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||
// every other level answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (compressedFormat != GL_NONE)
|
||||
? (GLint)compressedFormat
|
||||
@@ -3045,7 +3123,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -3059,7 +3138,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -3073,16 +3153,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||
// every other level answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (GLfloat)((compressedFormat != GL_NONE)
|
||||
? compressedFormat
|
||||
@@ -3301,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
|
||||
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
|
||||
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
|
||||
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
|
||||
switch (target) {
|
||||
case GL_RENDERBUFFER:
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!mipmapTexture) return textureObject->GetBaseSize();
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
|
||||
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
|
||||
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
|
||||
// the ~30 entry points that reach it through a BOUND object (where the name was never
|
||||
// in question and the fault is the binding), so this is a local rule rather than a
|
||||
// change to the helper.
|
||||
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} name does not correspond to an existing image object.", endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
|
||||
// does not match the object INVALID_ENUM, where the shared uniformity helper records
|
||||
// INVALID_OPERATION for the upload paths that share it.
|
||||
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target, const char* endpointName) {
|
||||
if (!textureObject || textureObject->GetTarget() == target) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} target {} does not match the target the object was created with ({}).",
|
||||
endpointName, MG_Util::ConvertTextureTargetToString(target),
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
@@ -3315,14 +3468,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
|
||||
return false;
|
||||
}
|
||||
// ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
|
||||
// see that this particular texture stops at level 0. Both backends turn <level> into an
|
||||
// image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
|
||||
// DirectGLES forwards it to the ES copy), so a level the texture never had reached the
|
||||
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
|
||||
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
|
||||
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
|
||||
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||
return false;
|
||||
}
|
||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -3333,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
|
||||
// A multisample image can only be copied to one with the same sample count, and a
|
||||
// single-sample image reports zero - so this one comparison is also what rejects
|
||||
// copying between a multisample target and a non-multisample one.
|
||||
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("The two images have different sample counts ({} vs. {}).",
|
||||
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||
return false;
|
||||
}
|
||||
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
||||
// and no defined storage to copy into.
|
||||
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3403,7 +3609,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
||||
}
|
||||
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(internalFormat, srcInternalFormat)) THROW_UNIMPL_EXCEPTION;
|
||||
// The validator has already recorded GL_INVALID_OPERATION; just decline. Throwing
|
||||
// here unwound a C++ exception through the C GL ABI and killed the process (see the
|
||||
// same reasoning at :604-609).
|
||||
if (!TextureImpl::ValidateCopyTexImageBaseFormatSubset(internalFormat, srcInternalFormat)) return false;
|
||||
|
||||
GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
||||
GLenum realInternalFormat = GL_RGBA8;
|
||||
@@ -3426,8 +3635,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLint border) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// 1D textures are not implemented by this backend set. Record the error the way every
|
||||
// other unsupported entry point does - throwing unwinds through the C GL ABI and kills
|
||||
// the process, which is never an acceptable answer to an unsupported call.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage1D",
|
||||
"1D textures are not supported by this implementation"));
|
||||
}
|
||||
|
||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
@@ -3437,10 +3651,170 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
// Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
|
||||
// compressed glTexImage2D) shadowed for this level. Same deviation as the image call it
|
||||
// patches: the uncompressed texel shadow beside it is NOT touched, because there is no
|
||||
// BC/ETC codec here to decode the incoming blocks with - so what changes is the image
|
||||
// glGetCompressedTexImage hands back, not what the level samples as. Marking the texels
|
||||
// dirty would therefore only re-upload bytes that did not change.
|
||||
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
// ======================= Converting ================================
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
// Zero block width doubles as "format is not a specific compressed format", the
|
||||
// INVALID_ENUM case - one lookup answers both questions.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
// A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
|
||||
// accepts one.
|
||||
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"A proxy target has no texture image to modify."));
|
||||
return;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "width and height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (compressedInfo.blockWidth == 0) {
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (textureMipmapObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
|
||||
// exactly this compressed format. That is also what makes the block arithmetic below
|
||||
// sound - the level's grid is measured with THIS format's block size.
|
||||
const GLenum levelFormat =
|
||||
textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
|
||||
if (levelFormat != format) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"format does not match the internal format of the texture image."));
|
||||
return;
|
||||
}
|
||||
|
||||
const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
// Written as a subtraction rather than `xoffset + width > levelSize.x()`: both operands
|
||||
// are application-supplied GLints, so the sum is free to overflow, and a signed overflow
|
||||
// is undefined behaviour that a compiler may resolve by assuming the check passes.
|
||||
// levelSize is our own and non-negative, and the offsets are known non-negative by the
|
||||
// time the subtraction runs, so this form cannot wrap.
|
||||
if (xoffset < 0 || yoffset < 0 || width > levelSize.x() - xoffset || height > levelSize.y() - yoffset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The replaced region does not lie within the texture image."));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
|
||||
// and must either be a whole number of blocks wide/high or run to the image's edge.
|
||||
const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
|
||||
const Bool alignedX = (xoffset % blockWidth == 0) &&
|
||||
(width % blockWidth == 0 || xoffset + width == levelSize.x());
|
||||
const Bool alignedY = (yoffset % blockHeight == 0) &&
|
||||
(height % blockHeight == 0 || yoffset + height == levelSize.y());
|
||||
if (!alignedX || !alignedY) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The replaced region is not aligned to the format's compressed blocks."));
|
||||
return;
|
||||
}
|
||||
// Exactly the size the format and dimensions imply, which is also what keeps the copy
|
||||
// below in bounds.
|
||||
const SizeT expectedImageSize =
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
|
||||
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"imageSize does not match the compressed image size."));
|
||||
return;
|
||||
}
|
||||
|
||||
// ======================= Processing ================================
|
||||
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
|
||||
const void* compressedBytes = CompressedUnpackSource(data);
|
||||
if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
|
||||
if (compressedBytes == nullptr) {
|
||||
// No unpack buffer and a null client pointer: there is nothing to read. GL leaves
|
||||
// this undefined rather than erroring, and dereferencing it is the one answer that
|
||||
// is never acceptable.
|
||||
MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Once per process: the call is about to succeed, and what it does is narrower than what
|
||||
// an application has every right to expect from it. Before this existed the call answered
|
||||
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
|
||||
// sampled texels untouched is the kind of thing that costs a day to find from the other
|
||||
// end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
|
||||
// while the Log.h ordering compiled warnings out of the builds that ship.
|
||||
static std::atomic<Bool> announcedNoCodec{false};
|
||||
if (!announcedNoCodec.exchange(true)) {
|
||||
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
|
||||
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
|
||||
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
|
||||
"that.",
|
||||
__func__);
|
||||
}
|
||||
|
||||
// The level's compressed image is stored as one blob, so the rectangle is patched into
|
||||
// a copy of it and the whole thing handed back. Compressed sub-image uploads are not a
|
||||
// hot path, and this keeps the storage layer's compressed API to the two calls it has.
|
||||
const SizeT blobSize =
|
||||
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
const void* existing =
|
||||
textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
|
||||
if (blobSize == 0 || existing == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The texture level holds no compressed image to modify."));
|
||||
return;
|
||||
}
|
||||
Vector<Uint8> blob(blobSize);
|
||||
Memcpy(blob.data(), existing, blobSize);
|
||||
|
||||
const SizeT blockByteSize = compressedInfo.blockByteSize;
|
||||
const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
|
||||
compressedInfo.blockWidth;
|
||||
const SizeT levelRowBytes = levelBlocksX * blockByteSize;
|
||||
const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
|
||||
compressedInfo.blockWidth;
|
||||
const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
|
||||
compressedInfo.blockHeight;
|
||||
const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
|
||||
const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
|
||||
const SizeT regionRowBytes = regionBlocksX * blockByteSize;
|
||||
const auto* source = static_cast<const Uint8*>(compressedBytes);
|
||||
for (SizeT row = 0; row < regionBlocksY; ++row) {
|
||||
const SizeT destOffset = (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
|
||||
if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
|
||||
Memcpy(blob.data() + destOffset, source + row * regionRowBytes, regionRowBytes);
|
||||
}
|
||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
|
||||
blob.data(), blobSize);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
@@ -3533,28 +3907,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// SetMipmapCompressedImage re-arms it.
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
|
||||
|
||||
const void* compressedBytes = data;
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
if (pixelUnpackBufferObject->IsMapped()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Pixel unpack buffer is currently mapped."));
|
||||
return;
|
||||
}
|
||||
const SizeT offset = reinterpret_cast<SizeT>(data);
|
||||
const SizeT bufferSize = pixelUnpackBufferObject->GetSize();
|
||||
if (offset > bufferSize || expectedImageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Unpacking would read past the end of the pixel unpack buffer."));
|
||||
return;
|
||||
}
|
||||
compressedBytes = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) + offset;
|
||||
}
|
||||
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
|
||||
const void* compressedBytes = CompressedUnpackSource(data);
|
||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
|
||||
expectedImageSize);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
@@ -4064,6 +4418,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// core 8.19). Allocating only the primary one left the object cube-incomplete, so every
|
||||
// framebuffer it was attached to reported GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. Every other
|
||||
// 2D target has exactly one upload target, so this loop is a no-op change for them.
|
||||
// A specific compressed internalformat commits every level it allocates to that
|
||||
// format, the same way glTexImage2D does - and here it matters twice over, because
|
||||
// immutable storage plus glCompressedTexSubImage2D IS the modern way to upload a
|
||||
// compressed texture: without the tag that sub-image call finds an uncompressed
|
||||
// level and refuses it. Zero width means a generic (implementation's choice)
|
||||
// format, which MobileGL answers with uncompressed storage, so it is not tagged.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
|
||||
for (const auto uploadTarget : textureObject->GetUploadTargets()) {
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
@@ -4072,6 +4433,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
|
||||
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||
if (compressedInfo.blockWidth != 0) {
|
||||
// After AllocateStorage, which clears the tag.
|
||||
textureMipmapObject->SetMipmapCompressedImage(
|
||||
uploadTarget, static_cast<Uint>(level), internalformat, nullptr,
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
||||
{levelWidth, levelHeight, 1}));
|
||||
}
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||
@@ -4079,10 +4447,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
|
||||
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
|
||||
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
|
||||
// .compressed_data). Written against the enum ranges rather than a name list because the
|
||||
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
|
||||
// carry, which would make this check silently narrower than the API surface.
|
||||
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case 0x8225: // GL_COMPRESSED_RED
|
||||
case 0x8226: // GL_COMPRESSED_RG
|
||||
case 0x84ED: // GL_COMPRESSED_RGB
|
||||
case 0x84EE: // GL_COMPRESSED_RGBA
|
||||
case 0x8C48: // GL_COMPRESSED_SRGB
|
||||
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
|
||||
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
|
||||
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
|
||||
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
|
||||
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
|
||||
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
|
||||
}
|
||||
|
||||
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
GLsizei depth) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
if (textureObject->GetTarget() == TextureTarget::Texture3D &&
|
||||
IsCompressedGLInternalFormat(internalformat)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("{} is a compressed internal format and cannot back GL_TEXTURE_3D storage.",
|
||||
MG_Util::ConvertGLEnumToString(internalformat))));
|
||||
return;
|
||||
}
|
||||
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
||||
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
||||
@@ -4405,6 +4809,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
free(processedPixels);
|
||||
}
|
||||
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
|
||||
CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data);
|
||||
});
|
||||
}
|
||||
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
@@ -4482,6 +4894,132 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
|
||||
}
|
||||
|
||||
GLint GetCombinedTextureImageUnitCount() {
|
||||
GLint maxTextureUnits = 0;
|
||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ARB_multi_bind checks the whole [first, first + count) range before binding anything and
|
||||
// reports an overrun as INVALID_OPERATION - not the INVALID_VALUE the single-bind entry
|
||||
// points report for an out-of-range unit, and not after binding the in-range prefix.
|
||||
Bool ValidateMultiBindUnitRange(GLuint first, GLsizei count, GLint unitCount, const char* funcName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(unitCount)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
std::format("first + count ({} + {}) exceeds the {} available units.",
|
||||
first, count, unitCount)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ARB_multi_bind states the equivalence to a loop of single binds "except that <textures>
|
||||
// will not be created if they do not exist": glBindTexture instantiates a name GenTextures
|
||||
// merely reserved, the multi-bind entry points must refuse it. The error class is
|
||||
// INVALID_OPERATION for both of them, where the scalar glBindImageTexture reports
|
||||
// INVALID_VALUE - hence the check here rather than inside BindImageTexture.
|
||||
//
|
||||
// Deliberately PER ELEMENT: the extension defines these calls as a loop, so a bad entry
|
||||
// costs its own unit and leaves the rest of the range bound.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> ResolveMultiBindTexture(GLuint texture, GLsizei index,
|
||||
const char* funcName) {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
std::format("textures[{}] ({}) is not the name of an existing texture object.", index,
|
||||
texture)));
|
||||
}
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
// ARB_multi_bind: an element naming texture zero unbinds EVERY target of its unit, i.e.
|
||||
// rebinds each target's default texture object - the unit's initial state. Same rule
|
||||
// glBindTextureUnit(unit, 0) follows.
|
||||
void UnbindAllTargetsOnUnit(Int unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
Bool changed = false;
|
||||
for (auto& slot : textureUnit.GetAllBindingSlots()) {
|
||||
if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
|
||||
}
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ARB_multi_bind: glBindTextures binds each texture to ITS OWN target on unit <first> + i, so
|
||||
// there is no target parameter and no way to express it through glBindTexture - the per-unit,
|
||||
// by-object form glBindTextureUnit uses is the one that matches. A NULL <textures> unbinds the
|
||||
// whole range.
|
||||
void BindTextures(GLuint first, GLsizei count, const GLuint* textures) {
|
||||
if (!ValidateMultiBindUnitRange(first, count, GetCombinedTextureImageUnitCount(), __func__)) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
const GLuint texture = textures ? textures[i] : 0;
|
||||
const Int unit = static_cast<Int>(first) + i;
|
||||
if (texture == 0) {
|
||||
UnbindAllTargetsOnUnit(unit);
|
||||
continue;
|
||||
}
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
ResolveMultiBindTexture(texture, i, __func__);
|
||||
if (!textureObject) continue;
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
|
||||
}
|
||||
}
|
||||
|
||||
// ARB_multi_bind: glBindImageTextures is a loop of glBindImageTexture with every parameter but
|
||||
// the unit and the texture fixed by the spec - level 0, layered, layer 0, READ_WRITE, and the
|
||||
// texture's own internal format. An element that names texture zero resets the unit.
|
||||
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures) {
|
||||
if (!ValidateMultiBindUnitRange(first, count, static_cast<GLint>(GetAdvertisedImageUnitCount()), __func__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
const GLuint texture = textures ? textures[i] : 0;
|
||||
const GLuint unit = first + static_cast<GLuint>(i);
|
||||
if (texture == 0) {
|
||||
BindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
|
||||
continue;
|
||||
}
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
|
||||
ResolveMultiBindTexture(texture, i, __func__);
|
||||
if (!textureObject) continue;
|
||||
|
||||
// "An INVALID_OPERATION error is generated if the internal format of any texture is not
|
||||
// supported for image textures" - a texture that has never been given storage has no
|
||||
// format at all and lands here too, rather than being reported as a bad enum by the
|
||||
// scalar path.
|
||||
const GLenum format = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
if (!IsValidImageTextureFormat(format)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("textures[{}] ({}) has an internal format that is not supported for image "
|
||||
"textures.",
|
||||
i, texture)));
|
||||
continue;
|
||||
}
|
||||
BindImageTexture(unit, texture, 0, GL_TRUE, 0, GL_READ_WRITE, format);
|
||||
}
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
@@ -5176,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
||||
srcWidth, srcHeight, srcDepth)) {
|
||||
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||
// SharedPtr, and let the validator record the error this entry point owes.
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
|
||||
MG_State::pGLContext->GetTextureObject(srcName);
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
|
||||
MG_State::pGLContext->GetTextureObject(dstName);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum format, GLenum type, const void* pixels);
|
||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
|
||||
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
|
||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||
@@ -132,5 +134,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||
GLsizei imageSize, const void* data);
|
||||
void BindTexture(GLenum target, GLuint texture);
|
||||
void BindTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
void ActiveTexture(GLenum texture);
|
||||
// The number of texture image units a texture or a sampler may be bound to: what the backend
|
||||
// advertises as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, clamped by the frontend's fixed unit-array
|
||||
// capacity. Shared so the texture and sampler multi-bind range checks cannot drift apart.
|
||||
GLint GetCombinedTextureImageUnitCount();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureTarget(TextureTarget target) {
|
||||
@@ -353,6 +354,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller) {
|
||||
// A null object is somebody else's error to report - ValidateTextureObject runs
|
||||
// first at every call site and has already recorded it.
|
||||
if (!textureObject) return false;
|
||||
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (mipmapTexture == nullptr) {
|
||||
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
|
||||
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
|
||||
// set). Declining here is not the error code the spec asks for - that would be
|
||||
// INVALID_ENUM from a target check this validator is not - but it does keep a
|
||||
// texture with no image levels whatsoever from reaching a backend that would
|
||||
// dereference a backend texture it never created.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no mipmap levels to address."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// What this number is, exactly, because two other things are almost it and neither is
|
||||
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
|
||||
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
|
||||
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
|
||||
//
|
||||
// The guarantee that matters is one-sided: this count is always >= the level count the
|
||||
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
|
||||
// with a non-positive extent, so it can only be shorter). That is the safe direction -
|
||||
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
|
||||
// exact match, so the backends keep their own range guard for the band in between: a
|
||||
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
|
||||
// declined by the backend, which is a silent no-op rather than a copy. That band is a
|
||||
// backend storage limitation, not a validation one - rejecting it here with
|
||||
// INVALID_VALUE would be refusing a copy the spec permits.
|
||||
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
|
||||
if (levelCount == 0) {
|
||||
// No image has ever been defined on this texture, so the fault is the texture,
|
||||
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
|
||||
// copy names is an incomplete texture.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no image defined at any level."));
|
||||
return false;
|
||||
}
|
||||
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture level does not exist in this texture."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -424,19 +482,141 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||
auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||
auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||
if (unsizedFormat1 != unsizedFormat2) {
|
||||
namespace {
|
||||
// Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6
|
||||
// reasons about. Colour components are independent bits so "subset" is a plain
|
||||
// mask test; depth and stencil are their own components and never satisfy a
|
||||
// colour request (or each other).
|
||||
enum : Uint32 {
|
||||
kComponentR = 1u << 0,
|
||||
kComponentG = 1u << 1,
|
||||
kComponentB = 1u << 2,
|
||||
kComponentA = 1u << 3,
|
||||
kComponentDepth = 1u << 4,
|
||||
kComponentStencil = 1u << 5,
|
||||
};
|
||||
|
||||
Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) {
|
||||
switch (unsizedFormat) {
|
||||
case TextureInternalFormat::Red:
|
||||
return kComponentR;
|
||||
case TextureInternalFormat::RG:
|
||||
return kComponentR | kComponentG;
|
||||
case TextureInternalFormat::RGB:
|
||||
return kComponentR | kComponentG | kComponentB;
|
||||
case TextureInternalFormat::RGBA:
|
||||
return kComponentR | kComponentG | kComponentB | kComponentA;
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
return kComponentDepth;
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
return kComponentDepth | kComponentStencil;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||
CopyImageTexelBlock block{};
|
||||
if (compressedFormat != GL_NONE) {
|
||||
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||
if (info.blockByteSize != 0) {
|
||||
block.byteSize = info.blockByteSize;
|
||||
block.blockWidth = info.blockWidth;
|
||||
block.blockHeight = info.blockHeight;
|
||||
block.compressed = true;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||
// copy between them would in fact preserve the bytes.
|
||||
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||
return block;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock) {
|
||||
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
"A copied image has no storage whose texel size is known."));
|
||||
return false;
|
||||
}
|
||||
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||
"The base internal format of the two formats do not match ({} vs. {})",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str())));
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||
"formats are not copy-compatible.",
|
||||
srcBlock.byteSize, dstBlock.byteSize)));
|
||||
return false;
|
||||
}
|
||||
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||
if (srcBlock.compressed && dstBlock.compressed &&
|
||||
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||
dstBlock.blockHeight)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} // namespace TextureImpl
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||
if (!block.compressed) return true;
|
||||
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||
// whole even when the last block is partial.
|
||||
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||
if (originAligned && widthOk && heightOk) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||
"grid of a {} x {} image.",
|
||||
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||
// GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components,
|
||||
// not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what
|
||||
// Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate
|
||||
// above and turn its rejection into an uncaught exception through the C GL ABI, so the
|
||||
// app died rather than seeing a GL error.
|
||||
const Uint32 destComponents = BaseFormatComponents(unsizedDest);
|
||||
const Uint32 srcComponents = BaseFormatComponents(unsizedSrc);
|
||||
if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset",
|
||||
std::format("the read buffer's base internal format {} does not provide every component of "
|
||||
"the requested internal format {}",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedSrc),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedDest))));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
|
||||
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
|
||||
// about the object. Entry points that resolve a level straight into a backend image
|
||||
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
|
||||
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
|
||||
// Note the error split is per-entry-point, so this is not universally reusable:
|
||||
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
|
||||
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller);
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
@@ -40,5 +50,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||
struct CopyImageTexelBlock {
|
||||
SizeT byteSize = 0;
|
||||
Uint blockWidth = 1;
|
||||
Uint blockHeight = 1;
|
||||
Bool compressed = false;
|
||||
};
|
||||
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock);
|
||||
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||
// the edge of the image.
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||
// the requested internalformat asks for, but may supply more.
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -179,6 +179,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return vao;
|
||||
}
|
||||
|
||||
// The ARB_vertex_attrib_binding entry points that take no vertex array name modify the
|
||||
// *bound* vertex array, and in a core profile the default vertex array (name 0) is not
|
||||
// one: every one of them is INVALID_OPERATION there (GL 4.6 core 10.3.1, and the tail of
|
||||
// each KHR-GL4x.vertex_attrib_binding.negative-* case checks exactly this). MobileGL
|
||||
// keeps a real object at name 0 for the compatibility paths, so GetBoundVertexArray
|
||||
// never returns null and the rule has to be spelled out - behind the same gate the VAO-0
|
||||
// draw rule already uses (MOBILEGL_RELAXED_SEMANTICS, plus "the context never asked for
|
||||
// a core profile"), so applications that legitimately run relaxed keep working.
|
||||
static SharedPtr<MG_State::GLState::VertexArrayObject> GetBoundVertexArrayForBindingApi(const char* funcName) {
|
||||
auto vao = GetBoundVertexArrayOrError(funcName);
|
||||
if (!vao) return nullptr;
|
||||
if (vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", funcName,
|
||||
"The default vertex array object cannot be modified in a core profile."));
|
||||
return nullptr;
|
||||
}
|
||||
return vao;
|
||||
}
|
||||
|
||||
static bool ValidateVertexAttribPname(GLenum pname) {
|
||||
switch (pname) {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||
@@ -293,9 +315,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto offset = reinterpret_cast<SizeT>(pointer);
|
||||
|
||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
|
||||
const int effectiveStride = EffectiveVertexStride(stride, size, type);
|
||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false, effectiveStride);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
|
||||
}
|
||||
|
||||
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
|
||||
@@ -323,9 +346,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver.
|
||||
const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
||||
const int effectiveSize = isBgra ? 4 : size;
|
||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
|
||||
const int effectiveStride = EffectiveVertexStride(stride, effectiveSize, type);
|
||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra,
|
||||
effectiveStride);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
|
||||
}
|
||||
|
||||
void BindVertexArray_State(GLuint array) {
|
||||
@@ -502,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (!MG_Backend::pActiveBackendObject ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
"backend has no double-precision vertex attribute support - see the "
|
||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||
attribindex);
|
||||
@@ -944,7 +969,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[0] = static_cast<GLfloat>(attr->Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = static_cast<GLfloat>(attr->Stride);
|
||||
params[0] = static_cast<GLfloat>(attr->LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1014,7 +1039,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[0] = static_cast<GLdouble>(attr->Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = static_cast<GLdouble>(attr->Stride);
|
||||
params[0] = static_cast<GLdouble>(attr->LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1079,8 +1104,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||
params[0] = attr->Size;
|
||||
return;
|
||||
// The legacy shadow, not the resolved draw stride: GL 4.6 core table 23.3 defines this
|
||||
// as the last glVertexAttrib*Pointer argument, which glBindVertexBuffer must not
|
||||
// overwrite even though it does overwrite what the backend actually reads.
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
params[0] = attr->Stride;
|
||||
params[0] = attr->LegacyStride;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||
@@ -1138,7 +1166,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto& attr = vao->GetAttribute(index);
|
||||
*pointer = reinterpret_cast<void*>(attr.Offset);
|
||||
*pointer = reinterpret_cast<void*>(attr.LegacyPointer);
|
||||
}
|
||||
|
||||
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
||||
@@ -1222,7 +1250,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*param = static_cast<GLint>(attr.Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
*param = static_cast<GLint>(attr.Stride);
|
||||
*param = static_cast<GLint>(attr.LegacyStride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||
@@ -1294,14 +1322,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
|
||||
if (!vao) return;
|
||||
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
||||
}
|
||||
|
||||
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||
const GLsizei* strides) {
|
||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
@@ -1315,21 +1343,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
||||
"VertexAttribFormat");
|
||||
}
|
||||
|
||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
||||
"VertexAttribIFormat");
|
||||
}
|
||||
|
||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||
}
|
||||
@@ -1341,7 +1369,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
|
||||
if (!vao) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
||||
@@ -1349,7 +1377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor");
|
||||
auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
||||
vao->SetBindingDivisor(bindingindex, divisor);
|
||||
|
||||
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||
|
||||
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCopyContext is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||
MGLOG_W("glx: glXCreatePixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||
MGLOG_W("glx: glXUseXFont is not supported");
|
||||
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
||||
}
|
||||
if (!fns->Valid()) {
|
||||
MGLOG_E("glx: failed to load libX11 entry points");
|
||||
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
||||
}
|
||||
return fns;
|
||||
}();
|
||||
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
||||
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
width, height);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("glx: no EGL display");
|
||||
MGLOG_E_ONCE("glx: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
||||
configCount <= 0) {
|
||||
MGLOG_E("glx: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("glx: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
||||
object->Context)) {
|
||||
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
return 0;
|
||||
}
|
||||
t_current = {dpy, drawable, drawable, context};
|
||||
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
if (context && draw != read) {
|
||||
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
||||
// drawable for both, which is what every real caller here needs.
|
||||
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
read);
|
||||
}
|
||||
const int result = MakeCurrent(dpy, draw, context);
|
||||
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
auto& surfaces = DrawableSurfaces();
|
||||
auto it = surfaces.find(drawable);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
return;
|
||||
}
|
||||
SyncSurfaceSize(dpy, drawable, it->second);
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
|
||||
#endif
|
||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
||||
MGLOG_D("Failed to get function: %s", (const char*)name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
||||
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
||||
|
||||
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
|
||||
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
||||
if (!metalLayerClass) {
|
||||
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::SetCurrentContext(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::FlushDrawable(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
return;
|
||||
}
|
||||
CGLImpl::UpdateContext(context);
|
||||
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getInstanceMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
if (original) {
|
||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getClassMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
method_setImplementation(method, replacement);
|
||||
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||
if (!pixelFormatClass || !contextClass) {
|
||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||
MGLOG_W("wglCopyContext is not supported");
|
||||
MGLOG_W_ONCE("wglCopyContext is not supported");
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryClientSize(hwnd, width, height)) {
|
||||
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLSurface surface =
|
||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("wgl: no EGL display");
|
||||
MGLOG_E_ONCE("wgl: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLConfig config = nullptr;
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
||||
MGLOG_E("wgl: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("wgl: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("wgl: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
auto& surfaces = WindowSurfaces();
|
||||
auto it = surfaces.find(hwnd);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
SyncSurfaceSize(hwnd, it->second);
|
||||
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
}
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
||||
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
return FALSE;
|
||||
}
|
||||
t_current = {hdc, hglrc};
|
||||
|
||||
@@ -50,9 +50,38 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/CrossFrameBufferScenario.cpp
|
||||
Scenarios/ResidentIndexScenario.cpp
|
||||
Scenarios/MultiDrawScenario.cpp
|
||||
Scenarios/DrawParametersScenario.cpp
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
Scenarios/AdvertisedLimitsScenario.cpp
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/ViewportArrayScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/ImageFormatQualifierScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
Scenarios/BufferTextureScenario.cpp
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -169,25 +198,24 @@ endif()
|
||||
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
||||
|
||||
# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default
|
||||
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
||||
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
||||
# fails and every scenario skips.
|
||||
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
else()
|
||||
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||
endif()
|
||||
# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless
|
||||
# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a
|
||||
# developer's machine and a CI runner take the SAME path whether or not a window
|
||||
# system happens to be running. This used to inject "x11", which is how the lane
|
||||
# came up green on a workstation with WSLg and died on a runner with no X server.
|
||||
#
|
||||
# A build-system knob would not just be redundant, it would be a trap: `set(...
|
||||
# CACHE ...)` does not rewrite an existing cache, so every build directory
|
||||
# configured before this change would keep injecting EGL_PLATFORM=x11 and go on
|
||||
# binding to a window system - silently, and only on the machines that have one.
|
||||
# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own
|
||||
# environment, which the harness still honours.
|
||||
|
||||
set(MGL_ITEST_COMMON_ENV "")
|
||||
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||
endif()
|
||||
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
||||
endif()
|
||||
unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11
|
||||
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||
endif()
|
||||
@@ -221,6 +249,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
@@ -268,3 +298,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# A fourth registration, of the depth/stencil readback scenarios, with the ES
|
||||
# shader-sampling emulation forced on. Not paranoia - without it these scenarios are
|
||||
# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil
|
||||
# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them
|
||||
# goes green through a native path that the Adreno device does not have. Deleting the
|
||||
# entire emulation left all of them passing. With the flag the native spellings are off
|
||||
# the table and only the path the device actually takes remains. DirectGLES only - the
|
||||
# emulation is DirectGLES's.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation."
|
||||
TEST_FILTER "DepthStencilReadback*Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -74,6 +74,40 @@ namespace MGITest {
|
||||
std::string renderer;
|
||||
};
|
||||
|
||||
// The harness is headless BY CONSTRUCTION, on every machine: it must never
|
||||
// reach a window system, not even where one happens to be running. This is
|
||||
// not a CI accommodation - it is what keeps a developer's run and a CI run
|
||||
// the same run. The lane was wired up green on a workstation and immediately
|
||||
// died on the runner precisely because the workstation had a DISPLAY (WSLg)
|
||||
// and took Mesa's x11 platform, while the runner has none; that divergence
|
||||
// is the bug, and pinning the platform here is the fix for it.
|
||||
//
|
||||
// Mesa selects its EGL platform from EGL_PLATFORM at loader time, so this
|
||||
// has to run before the first EGL call in the process (see EnsureHeadless
|
||||
// callers). surfaceless is the platform with no window-system dependency at
|
||||
// all; the surface this file then creates is still a pbuffer, which every
|
||||
// platform supports and which the amendment to this rule requires as the
|
||||
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||
// driver that consults them directly cannot reintroduce the dependency
|
||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
||||
// for Android, so no device path is affected.
|
||||
void EnsureHeadlessPlatform() {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
static bool done = false;
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
// An explicit EGL_PLATFORM from the operator still wins: pinning a
|
||||
// platform is exactly how someone reproduces a platform-specific bug.
|
||||
if (std::getenv("EGL_PLATFORM") == nullptr) {
|
||||
setenv("EGL_PLATFORM", "surfaceless", 1);
|
||||
}
|
||||
unsetenv("DISPLAY");
|
||||
unsetenv("WAYLAND_DISPLAY");
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE bring-up, in one function so the pre-flight child and the parent run
|
||||
// literally the same sequence - a pre-flight that tests something narrower
|
||||
// than what the parent will do is exactly the kind of "predictive" check
|
||||
@@ -82,6 +116,9 @@ namespace MGITest {
|
||||
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||
// fills outReason either way.
|
||||
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
||||
// Belt and braces: the pre-flight child and the parent both enter here,
|
||||
// and neither may be the first to touch EGL without this having run.
|
||||
EnsureHeadlessPlatform();
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||
@@ -199,11 +236,10 @@ namespace MGITest {
|
||||
}
|
||||
if (child == 0) {
|
||||
close(channel[0]);
|
||||
// The child is EXPECTED to die on a signal on an unusable
|
||||
// platform; that is the measurement. Do not let each such
|
||||
// measurement drop a core file next to the test binary.
|
||||
const rlimit noCore{0, 0};
|
||||
setrlimit(RLIMIT_CORE, &noCore);
|
||||
// No core suppression here, deliberately: when the child dies on a
|
||||
// signal, the core IS the diagnosis (an rlimit that used to sit here
|
||||
// made a CI-only crash undebuggable). Machines that do not want
|
||||
// cores control that with the usual ulimit/core_pattern knobs.
|
||||
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||
EglBringUp local;
|
||||
std::string reason;
|
||||
@@ -284,9 +320,19 @@ namespace MGITest {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
bool EnvFlag(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool RequireGpu() {
|
||||
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||
}
|
||||
|
||||
bool RequireHardwareGpu() {
|
||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU");
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||
@@ -390,6 +436,10 @@ namespace MGITest {
|
||||
}
|
||||
|
||||
HeadlessGL::HeadlessGL() {
|
||||
// Before anything else in this process can reach EGL, and in particular
|
||||
// before the pre-flight forks - the child must measure the same platform
|
||||
// the parent will use.
|
||||
EnsureHeadlessPlatform();
|
||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||
m_usable = BringUp();
|
||||
}
|
||||
@@ -551,9 +601,13 @@ namespace MGITest {
|
||||
}
|
||||
|
||||
Image ReadPixels(int width, int height) {
|
||||
return ReadPixelsRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
Image ReadPixelsRect(int x, int y, int width, int height) {
|
||||
Image image(width, height);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,15 @@ namespace MGITest {
|
||||
// a job that ran everything.
|
||||
bool RequireGpu();
|
||||
|
||||
// True when MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set: additionally asserts
|
||||
// that the context did NOT land on a software rasterizer. Deliberately a
|
||||
// SEPARATE switch from RequireGpu - a GPU-less CI runner is a supported and
|
||||
// intended configuration for these scenarios (they pin backend draw logic,
|
||||
// which llvmpipe/lavapipe execute faithfully), so CI wants the falsifiability
|
||||
// of REQUIRE_GPU without the hardware demand. Use this one only where a vendor
|
||||
// pin silently degrading to software would invalidate the measurement.
|
||||
bool RequireHardwareGpu();
|
||||
|
||||
struct Rgba8 {
|
||||
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
@@ -175,11 +184,18 @@ namespace MGITest {
|
||||
|
||||
void ClearTo(float r, float g, float b, float a);
|
||||
|
||||
// Reads back the whole currently bound READ framebuffer. width/height must
|
||||
// be the target's full size - DirectVulkan's default-framebuffer readback
|
||||
// only re-orients a full-extent read.
|
||||
// Reads back the whole currently bound READ framebuffer.
|
||||
Image ReadPixels(int width, int height);
|
||||
|
||||
// A PARTIAL glReadPixels. Row 0 of the returned image is GL row `y` of the
|
||||
// framebuffer, i.e. the bottom row of the requested rect - the same
|
||||
// convention ReadPixels uses, just with an origin. This is the shape the
|
||||
// conformance suite reads in (a random sub-rect of the default
|
||||
// framebuffer), and the shape DirectVulkan's default-FBO readback used to
|
||||
// hand back in Vulkan row order because its re-orientation only ran on an
|
||||
// exact full-extent read.
|
||||
Image ReadPixelsRect(int x, int y, int width, int height);
|
||||
|
||||
// Drains any GL error queue and returns the first error, or 0.
|
||||
unsigned int FirstGLError();
|
||||
const char* GLErrorName(unsigned int error);
|
||||
|
||||
@@ -45,12 +45,18 @@ namespace MGITest {
|
||||
}
|
||||
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||
}
|
||||
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
||||
// a misconfigured vendor pin silently lands on the software
|
||||
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
||||
<< gl.RendererString();
|
||||
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
||||
// unusable harness is a failure, not a silent skip" - it is the
|
||||
// falsifiability switch, and CI is exactly where it belongs. But CI
|
||||
// runners have no GPU, so folding "must not be llvmpipe" into the
|
||||
// same switch made the CI lane unpassable by construction: the
|
||||
// scenarios pin backend draw logic, which a software rasterizer
|
||||
// executes just as faithfully. Landing on llvmpipe/lavapipe there is
|
||||
// the intended configuration, not a misconfiguration. A vendor pin
|
||||
// that must not silently degrade sets REQUIRE_HARDWARE_GPU.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
||||
<< "rasterizer: " << gl.RendererString();
|
||||
}
|
||||
// A scenario starts from a clean slate but shares the context (and so
|
||||
// the renderer's memos) with every other scenario in this process -
|
||||
|
||||
@@ -26,8 +26,14 @@ namespace {
|
||||
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||
if (gl.Usable()) {
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height());
|
||||
// EGL_PLATFORM is echoed because it is the invariant this harness
|
||||
// rests on: the run is headless on every machine, so a run that
|
||||
// silently bound to a workstation's window system is a different
|
||||
// run from CI's and must be visible as one in the log.
|
||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||
} else if (MGITest::RequireGpu()) {
|
||||
std::fprintf(stderr,
|
||||
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.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 limit we advertise is a promise, and an application will hold us to it."
|
||||
//
|
||||
// DirectVulkan copied Vulkan descriptor limits straight into the GL limit table. Those are not
|
||||
// the same quantity: Adreno answers maxPerStageDescriptorUniformBuffers at descriptor-indexing
|
||||
// scale, and GL_MAX_COMPUTE_UNIFORM_BLOCKS is a count an app will allocate. KHR-GL44.multi_bind
|
||||
// .dispatch_bind_buffers_base does exactly that - createsO(limit) buffers and splices O(limit)
|
||||
// UBO declarations into one compute shader - and spent ~14 s allocating before dying on
|
||||
// std::bad_alloc. Its sibling dispatch_bind_buffers_range hard-codes 4 buffers and passes.
|
||||
//
|
||||
// Two failure modes, one table:
|
||||
// - too LARGE: an unusable promise (the OOM above).
|
||||
// - too SMALL or negative: a uint32 limit that lost its top bit on the way to a signed Int -
|
||||
// UINT32_MAX arrived as -1, which every downstream std::min then accepted as "small enough".
|
||||
// A conformant GL 4.x implementation may never advertise below the spec minimum either.
|
||||
//
|
||||
// Every bound below is checked on BOTH backends, because the loader casts are shared and the
|
||||
// DirectGLES lane is the control: it takes its limits from a driver that already reports GL
|
||||
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
|
||||
// fails on both is a table bug.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
struct LimitBound {
|
||||
GLenum pname;
|
||||
const char* name;
|
||||
// The GL 4.x required minimum. A value below this is a conformance failure in its own
|
||||
// right, and is what a sign-flipped uint32 looks like.
|
||||
int minimum;
|
||||
// The largest value this implementation is willing to promise. Chosen well above every
|
||||
// desktop driver's answer, so it can only catch a descriptor-scale number.
|
||||
int ceiling;
|
||||
};
|
||||
|
||||
const std::vector<LimitBound>& BufferLimitTable() {
|
||||
static const std::vector<LimitBound> table = {
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
{GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 8, 256},
|
||||
{GL_MAX_TEXTURE_BUFFER_SIZE, "GL_MAX_TEXTURE_BUFFER_SIZE", 65536, 1 << 27},
|
||||
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", 16384, 1 << 30},
|
||||
// Already clamped before this campaign; in the table so a regression there is
|
||||
// caught by the same case.
|
||||
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE", 1 << 24, 512 * 1024 * 1024},
|
||||
{GL_MAX_TEXTURE_IMAGE_UNITS, "GL_MAX_TEXTURE_IMAGE_UNITS", 16, 32},
|
||||
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS", 48, 192},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
class AdvertisedLimitsScenario : public ScenarioTest {};
|
||||
|
||||
TEST_F(AdvertisedLimitsScenario, EveryBufferLimitIsWithinItsAdvertisedRange) {
|
||||
for (const LimitBound& bound : BufferLimitTable()) {
|
||||
GLint value = -424242;
|
||||
glGetIntegerv(bound.pname, &value);
|
||||
const unsigned int error = FirstGLError();
|
||||
EXPECT_EQ(error, GLenum(GL_NO_ERROR))
|
||||
<< bound.name << " is not answerable: " << GLErrorName(error);
|
||||
if (error != GL_NO_ERROR) continue;
|
||||
|
||||
EXPECT_GE(value, bound.minimum)
|
||||
<< bound.name << " = " << value << " is below the GL required minimum "
|
||||
<< bound.minimum << " (a negative or tiny value here is a uint32 limit that lost "
|
||||
"its top bit on the way to a signed Int)";
|
||||
EXPECT_LE(value, bound.ceiling)
|
||||
<< bound.name << " = " << value << " exceeds the ceiling " << bound.ceiling
|
||||
<< " this implementation is willing to promise - an application that allocates "
|
||||
"what we advertise will run out of memory";
|
||||
}
|
||||
}
|
||||
|
||||
// A per-stage block count is an amount of BINDING POINTS an application will use, so it
|
||||
// can never exceed the number of binding points that exist. GL 4.6 Table 23.64 states the
|
||||
// relation the other way round (MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS
|
||||
// >= every per-stage count), and DirectVulkan broke it by clamping the two families
|
||||
// independently: a device reporting 256 compute uniform blocks and 84 uniform binding
|
||||
// points passes both ceilings and still cannot serve
|
||||
// KHR-GL44.multi_bind.dispatch_bind_buffers_base, which reads the block count and binds
|
||||
// that many buffers in one glBindBuffersBase - INVALID_OPERATION before a single bind.
|
||||
TEST_F(AdvertisedLimitsScenario, PerStageBlockCountsFitInTheirBindingPoints) {
|
||||
struct Relation {
|
||||
GLenum blocks;
|
||||
const char* blocksName;
|
||||
GLenum bindings;
|
||||
const char* bindingsName;
|
||||
};
|
||||
const Relation relations[] = {
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"},
|
||||
};
|
||||
for (const Relation& relation : relations) {
|
||||
GLint blocks = -1;
|
||||
GLint bindings = -1;
|
||||
glGetIntegerv(relation.blocks, &blocks);
|
||||
glGetIntegerv(relation.bindings, &bindings);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.blocksName;
|
||||
EXPECT_LE(blocks, bindings)
|
||||
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
|
||||
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
|
||||
// binding points at MAX_<target>_SIZE / MAX_<target>_BINDINGS and binds all of them in
|
||||
// one glBindBuffersRange. That quotient has to be a legal BindBufferRange size, which
|
||||
// makes the two limits of every indexed family a PAIR: advertise a size that does not
|
||||
// survive division by the binding count and the call fails with INVALID_VALUE before any
|
||||
// of it binds.
|
||||
TEST_F(AdvertisedLimitsScenario, IndexedTargetSizeSurvivesDivisionByItsBindingCount) {
|
||||
struct IndexedFamily {
|
||||
GLenum maxSize;
|
||||
const char* maxSizeName;
|
||||
GLenum maxBindings;
|
||||
const char* maxBindingsName;
|
||||
GLint sizeGranularity; // BindBufferRange's size rule for the target
|
||||
};
|
||||
const IndexedFamily families[] = {
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE",
|
||||
GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS", 1},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS",
|
||||
GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS", 4},
|
||||
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BUFFER_BINDINGS,
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", 1},
|
||||
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE",
|
||||
GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 1},
|
||||
};
|
||||
for (const IndexedFamily& family : families) {
|
||||
GLint maxSize = -1;
|
||||
GLint maxBindings = -1;
|
||||
glGetIntegerv(family.maxSize, &maxSize);
|
||||
glGetIntegerv(family.maxBindings, &maxBindings);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << family.maxSizeName;
|
||||
ASSERT_GT(maxBindings, 0) << family.maxBindingsName;
|
||||
const GLint perBinding = maxSize / maxBindings;
|
||||
EXPECT_GT(perBinding, 0)
|
||||
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
|
||||
<< maxBindings << ") is zero, and BindBufferRange rejects a zero size";
|
||||
EXPECT_EQ(perBinding % family.sizeGranularity, 0)
|
||||
<< family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " ("
|
||||
<< maxBindings << ") = " << perBinding << " is not a multiple of the "
|
||||
<< family.sizeGranularity << "-byte size granularity BindBufferRange requires for it";
|
||||
}
|
||||
}
|
||||
|
||||
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
|
||||
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
|
||||
GLint blocks = -1;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &blocks);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(blocks, 12);
|
||||
EXPECT_LE(blocks, 256) << "KHR-GL44.multi_bind.dispatch_bind_buffers_base creates one GL buffer "
|
||||
"and one UBO declaration per advertised block";
|
||||
|
||||
GLint blockSize = -1;
|
||||
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &blockSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GT(blockSize, 0);
|
||||
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS is derived from the product of these two,
|
||||
// so their product has to stay representable.
|
||||
EXPECT_LE(static_cast<long long>(blocks) * blockSize,
|
||||
static_cast<long long>(2147483647))
|
||||
<< "blocks(" << blocks << ") * blockSize(" << blockSize << ") overflows the GLint the "
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
// different source, so all three are checked on both lanes.
|
||||
//
|
||||
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||
GLint maxViewports = -1;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||
"viewport, and the CTS sizes its arrays off this number";
|
||||
|
||||
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
|
||||
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||
// limit MobileGL currently passes through from the driver.
|
||||
GLint subpixelBits = -1;
|
||||
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||
"a negative value is what a sign-flipped uint32 looks like";
|
||||
|
||||
GLint viewportDims[2] = {-1, -1};
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint maxRenderbufferSize = -1;
|
||||
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -157,6 +157,22 @@ void main() {
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
||||
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
||||
class OptimisticStatusScope {
|
||||
public:
|
||||
explicit OptimisticStatusScope(const QuirkOverride mode)
|
||||
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||
}
|
||||
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
||||
|
||||
private:
|
||||
const QuirkOverride m_saved;
|
||||
};
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||
class CompilerThreadScope {
|
||||
@@ -463,5 +479,81 @@ void main() {
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// The Iris two-phase shape end to end on a real driver, with the optimistic-status
|
||||
// quirk on: phase 1 compiles each stage and reads its log then its status (both
|
||||
// answered optimistically), links, detaches and deletes the shaders for every
|
||||
// program with no program-level read anywhere; phase 2 then checks every link and
|
||||
// draws every program. Deliberately NOT built on the harness CompileProgram(),
|
||||
// whose status read would join and collapse the phase-1 overlap this exists to
|
||||
// exercise. What the unit suite cannot see - worker-produced artifacts the backend
|
||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||
// a compile into a no-op) but a pure pass-through at runtime: the bulk sits in
|
||||
// a branch a zero-initialised uniform never takes.
|
||||
const auto fragmentSource = [](const int index) {
|
||||
std::string source = "#version 330 core\nin vec3 vColor;\nout vec4 oColor;\n";
|
||||
source += "uniform float uGate" + std::to_string(index) + ";\n";
|
||||
source += "void main() {\n oColor = vec4(vColor, 1.0);\n";
|
||||
source += " if (uGate" + std::to_string(index) + " > 1e30) {\n float acc = 1.0;\n";
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
||||
}
|
||||
source += " oColor = vec4(acc);\n }\n}\n";
|
||||
return source;
|
||||
};
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
m_sources.push_back(fragmentSource(i));
|
||||
const char* fsText = m_sources.back().c_str();
|
||||
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
(void)ShaderInfoLog(vs); // Iris's exact order: the log first...
|
||||
(void)ShaderCompileStatus(vs); // ...then the status; both optimistic.
|
||||
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &fsText, nullptr);
|
||||
glCompileShader(fs);
|
||||
(void)ShaderInfoLog(fs);
|
||||
(void)ShaderCompileStatus(fs);
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glBindAttribLocation(program, 0, "aPos");
|
||||
glBindAttribLocation(program, 1, "aColor");
|
||||
glLinkProgram(program);
|
||||
glDetachShader(program, vs);
|
||||
glDetachShader(program, fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
programs.push_back(program);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<std::size_t>(i)];
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "program " << i;
|
||||
const Image image = DrawFrameWith(program);
|
||||
EXPECT_EQ(image.QuadrantSignature(), "blue,green,red,white") << "program " << i;
|
||||
}
|
||||
for (const GLuint program : programs) glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A BUFFER TEXTURE IS SAMPLED FROM THE VERTEX STAGE, AND TRACKS ITS BUFFER.
|
||||
//
|
||||
// Buffer textures are core in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
|
||||
// application may build geometry out of one without asking whether the host can. Minecraft
|
||||
// 26.3 does exactly that: its cloud layer has no vertex attributes at all, only gl_VertexID
|
||||
// and texelFetch on a GL_R8I buffer texture. Nothing covered that path end to end on either
|
||||
// backend - the frontend unit tests stop at glTexBuffer's state, and no scenario ever drew
|
||||
// with the result - which is how DirectGLES came to emit `#extension GL_EXT_texture_buffer :
|
||||
// require` unconditionally, compile nothing on a host without the extension, and lose the
|
||||
// whole cloud layer with no diagnostic anywhere.
|
||||
//
|
||||
// Two claims, in the order they can break:
|
||||
// 1. a vertex-stage texelFetch on an R8I buffer texture reads the byte the application put
|
||||
// in the buffer (the shape of the real workload: no attributes, index from gl_VertexID);
|
||||
// 2. a later glBufferSubData is visible to the next draw WITHOUT re-specifying the texture.
|
||||
// glTexBuffer attaches storage, it does not copy: the texture is a live view of the
|
||||
// buffer, so a backend that only refreshes the view when the texture's own state changes
|
||||
// must still show the new bytes. DirectGLES' respecify gate is keyed on the texture info
|
||||
// and deliberately does not include the buffer's contents, so this is the assertion that
|
||||
// says that is safe rather than merely untested.
|
||||
//
|
||||
// NOTE ON A HOST WITHOUT BUFFER TEXTURES: this scenario is expected to FAIL there, and that is
|
||||
// the honest outcome - MobileGL keeps advertising GL_MAX_TEXTURE_BUFFER_SIZE (an OpenGL 4.x
|
||||
// context may not answer 0), so there is no capability an application, or this test, could
|
||||
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// No vertex attributes: the quad's corners come from gl_VertexID, exactly like the
|
||||
// workload this exists for. The texel is fetched in the VERTEX stage - the stage where
|
||||
// buffer-texture support is scarcest across ES drivers - and carried flat so every
|
||||
// fragment of the quad reports the same byte and the readback is exact.
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
uniform isamplerBuffer uFaces;
|
||||
flat out int vFace;
|
||||
void main() {
|
||||
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||
vFace = texelFetch(uFaces, 0).r;
|
||||
gl_Position = vec4(corner, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// 1/255 steps survive an RGBA8 round trip exactly, so the readback byte IS the value
|
||||
// the vertex shader fetched.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
flat in int vFace;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
|
||||
// another, so a single dispatch proves the read direction (which already worked) and
|
||||
// the write direction (which is what this exists for) apart from each other.
|
||||
constexpr const char* kImageBufferCS = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 0, rgba8) uniform imageBuffer uImage;
|
||||
void main() {
|
||||
vec4 read = imageLoad(uImage, 1);
|
||||
imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
|
||||
imageStore(uImage, 2, read);
|
||||
}
|
||||
)";
|
||||
|
||||
class BufferTextureScenario : public ScenarioTest {
|
||||
protected:
|
||||
bool ComputeImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
unsigned int MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
};
|
||||
|
||||
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
|
||||
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
|
||||
// fetch, is what this test is measuring).
|
||||
int PaintedValue(unsigned int program, int width, int height) {
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
|
||||
const Image image = ReadPixels(width, height);
|
||||
if (image.Empty()) {
|
||||
return -1;
|
||||
}
|
||||
const int first = image.At(0, 0).r;
|
||||
for (int y = 0; y < image.Height(); ++y) {
|
||||
for (int x = 0; x < image.Width(); ++x) {
|
||||
if (image.At(x, y).r != first) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(BufferTextureScenario, VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// GL_R8I is the format the real workload uses. Signed, so the values stay well inside
|
||||
// [0, 127] to keep the readback arithmetic honest.
|
||||
constexpr signed char kInitial = 37;
|
||||
constexpr signed char kUpdated = 91;
|
||||
std::vector<signed char> texels(64, 0);
|
||||
texels[0] = kInitial;
|
||||
|
||||
// The harness shares one context across every scenario in the process, so an error left
|
||||
// by an earlier one would surface below as "glTexBuffer was refused".
|
||||
FirstGLError();
|
||||
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size()), texels.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R8I, buffer);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_R8I) was refused";
|
||||
|
||||
ColorFbo target = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
|
||||
BindFbo(target);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uFaces");
|
||||
ASSERT_NE(location, -1) << "the buffer sampler was optimized away or never reflected";
|
||||
glUniform1i(location, 0);
|
||||
|
||||
EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kInitial))
|
||||
<< "a vertex-stage texelFetch on an R8I buffer texture did not read the byte the "
|
||||
"application stored (a uniform -1 here means the quad was not uniform at all)";
|
||||
|
||||
// The texture is a VIEW of the buffer: no glTexBuffer call follows, and none should be
|
||||
// needed for the new bytes to be visible.
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferSubData(GL_TEXTURE_BUFFER, 0, 1, &kUpdated);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glBufferSubData on the texture's buffer was refused";
|
||||
|
||||
EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kUpdated))
|
||||
<< "the buffer texture kept showing the old contents after glBufferSubData; the "
|
||||
"texture must track its buffer without being re-specified";
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(target);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
|
||||
// land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
|
||||
// and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
|
||||
// and the shadow is refreshed on the next read; a buffer reached through an image unit is
|
||||
// the same write through a different binding, and Espryt used to flag only the first, so
|
||||
// an imageStore into a buffer texture was invisible to every CPU read that followed it -
|
||||
// silently, with the correct value sitting in the driver's buffer the whole time.
|
||||
//
|
||||
// The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
|
||||
// failure here cannot be blamed on the image binding not working at all.
|
||||
TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
|
||||
if (!Ready()) return;
|
||||
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";
|
||||
|
||||
constexpr GLuint kRed = 0x000000ffu; // RGBA8 little-endian: r = 255
|
||||
constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
|
||||
constexpr int kTexels = 16;
|
||||
|
||||
FirstGLError();
|
||||
|
||||
const unsigned int program = MakeComputeProgram(kImageBufferCS);
|
||||
ASSERT_NE(program, 0u);
|
||||
|
||||
const std::vector<GLuint> texels(kTexels, kRed);
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
|
||||
GL_DYNAMIC_COPY);
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";
|
||||
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
|
||||
// Both CPU read paths, because they are two entry points onto the same refresh and a
|
||||
// fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
|
||||
// ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
|
||||
// context across every scenario in the process, and a leaked buffer or image binding
|
||||
// here would surface as a failure somewhere else entirely.
|
||||
std::vector<GLuint> readBack(kTexels, 0u);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
|
||||
readBack.data());
|
||||
EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
|
||||
EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";
|
||||
|
||||
const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
|
||||
EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
|
||||
if (mapped != nullptr) {
|
||||
GLuint mappedTexel0 = 0;
|
||||
std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
|
||||
EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
|
||||
glUnmapBuffer(GL_TEXTURE_BUFFER);
|
||||
}
|
||||
|
||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,335 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A CLEAR OF THE DEFAULT FRAMEBUFFER IS VISIBLE TO glReadPixels WITH NO DRAW BETWEEN.
|
||||
//
|
||||
// DirectVulkan parks a glClear as a pending clear and folds it into the next render pass's
|
||||
// loadOp. When nothing is drawn after the clear there is no render pass, and the readback path
|
||||
// used to materialize pending clears only for USER framebuffers - so a readback right after a
|
||||
// clear of the DEFAULT framebuffer blitted the untouched swapchain image and handed back the
|
||||
// previous frame's colour.
|
||||
//
|
||||
// That is the whole of KHR-GL40.draw_indirect.negative-* (12 Magma failures): each case clears,
|
||||
// issues a draw that correctly raises INVALID_OPERATION and therefore never executes, then reads
|
||||
// the frame back expecting (0,0,0,0) and gets the previous case's (0.1,0.2,0.3,1). The staleness
|
||||
// cannot appear in one frame, so the scenario paints a frame first and clears in the next.
|
||||
//
|
||||
// The alpha assertion is the second half of the same census finding: a cleared default
|
||||
// framebuffer read back (0,0,0,1) where (0,0,0,0) was written, because the clear was routed
|
||||
// through the default FBO's placeholder attachment, whose format can lack alpha, rather than
|
||||
// through the swapchain image that actually has one.
|
||||
//
|
||||
// DirectGLES is the built-in control: a native GL driver has no deferred-clear model at all, so
|
||||
// a failure there would mean the scenario, not the backend.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// The colour KHR-GL40.draw_indirect's fshSimple paints, so a stale readback shows up as
|
||||
// the same value the conformance log reports.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.1, 0.2, 0.3, 1.0); }
|
||||
)";
|
||||
|
||||
class ClearThenReadPixelsScenario : public ScenarioTest {};
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// Frame 1: paint the whole default framebuffer, so there IS something stale to return.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
{
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
ASSERT_NEAR(centre.r, 26, 2) << "the setup frame did not paint; the staleness test would be vacuous";
|
||||
ASSERT_NEAR(centre.g, 51, 2);
|
||||
ASSERT_NEAR(centre.b, 77, 2);
|
||||
}
|
||||
gl.EndFrame();
|
||||
|
||||
// Frame 2: clear to transparent black and read back with NO draw at all.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
const Image cleared = ReadPixels(width, height);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
int nonZero = 0;
|
||||
int firstX = -1;
|
||||
int firstY = -1;
|
||||
Rgba8 firstOffender{};
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const Rgba8 pixel = cleared.At(x, y);
|
||||
if (pixel.r == 0 && pixel.g == 0 && pixel.b == 0 && pixel.a == 0) continue;
|
||||
if (nonZero == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstOffender = pixel;
|
||||
}
|
||||
++nonZero;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(nonZero, 0) << "glClear(0,0,0,0) followed by glReadPixels with no draw returned " << nonZero
|
||||
<< " of " << (width * height) << " non-zero pixels; first at (" << firstX << ", "
|
||||
<< firstY << ") = (" << static_cast<int>(firstOffender.r) << ", "
|
||||
<< static_cast<int>(firstOffender.g) << ", " << static_cast<int>(firstOffender.b)
|
||||
<< ", " << static_cast<int>(firstOffender.a) << ")";
|
||||
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// The same claim for a sub-rect read, which is the shape the conformance suite uses most and
|
||||
// the one whose orientation handling is separate (see OrientationScenario).
|
||||
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToASubRectReadback) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
DrawFullViewportQuad(program);
|
||||
gl.EndFrame();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
const int rectWidth = width / 2;
|
||||
const int rectHeight = height / 2;
|
||||
const Image cleared = ReadPixelsRect(width / 4, height / 4, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
int nonZero = 0;
|
||||
for (int y = 0; y < rectHeight; ++y) {
|
||||
for (int x = 0; x < rectWidth; ++x) {
|
||||
const Rgba8 pixel = cleared.At(x, y);
|
||||
if (pixel.r != 0 || pixel.g != 0 || pixel.b != 0 || pixel.a != 0) ++nonZero;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(nonZero, 0) << nonZero << " of " << (rectWidth * rectHeight)
|
||||
<< " pixels in a sub-rect read after a draw-free clear were not zero";
|
||||
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// The other half of the same rule, and the one the first version of this fix got wrong: a
|
||||
// parked clear must be executed BEFORE whatever writes the framebuffer next, not whenever the
|
||||
// readback happens to notice it. Minecraft clears the default framebuffer, renders the world
|
||||
// into its own framebuffer and blits the result out; nothing in between opens a render pass on
|
||||
// the default framebuffer, so the clear stays parked across the whole frame. Materializing it
|
||||
// at readback time therefore ran it AFTER the blit and returned a blank frame - which is what
|
||||
// took every DirectVulkan retrace to ssim 0.000005.
|
||||
TEST_F(ClearThenReadPixelsScenario, ABlitIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// Paint a source framebuffer, exactly as a game renders its world off-screen.
|
||||
ColorFbo source = MakeColorFbo(width, height);
|
||||
ASSERT_NE(source.fbo, 0u);
|
||||
BindFbo(source);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
|
||||
// Clear the DEFAULT framebuffer, then blit the source over it. The clear is white so a
|
||||
// frame that lost the blit is unmistakable, and the blit's colour is fshSimple's.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, source.fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image blitted = ReadPixels(width, height);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
const Rgba8 centre = blitted.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 26, 2) << "the blit into the default framebuffer did not survive the clear that "
|
||||
"preceded it; read back rgba(" << static_cast<int>(centre.r) << ", "
|
||||
<< static_cast<int>(centre.g) << ", " << static_cast<int>(centre.b) << ", "
|
||||
<< static_cast<int>(centre.a) << ")";
|
||||
EXPECT_NEAR(centre.g, 51, 2);
|
||||
EXPECT_NEAR(centre.b, 77, 2);
|
||||
|
||||
DestroyColorFbo(source);
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// A MULTISAMPLE-RESOLVE blit into the default framebuffer has to change orientation like any
|
||||
// other, but vkCmdResolveImage takes one offset per side and cannot invert an axis, so it used
|
||||
// to land the mirrored band. The renderer now resolves into a single-sample scratch image and
|
||||
// blits from there. The source is painted in two horizontal bands so the mirror is visible;
|
||||
// a full-extent uniform blit is a fixed point of the flip and would prove nothing.
|
||||
TEST_F(ClearThenReadPixelsScenario, AMultisampleResolveBlitIntoTheDefaultFramebufferKeepsItsOrientation) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
if (maxSamples < 2) {
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
|
||||
GLuint fbo = 0, rbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenRenderbuffers(1, &rbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 2, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
GTEST_SKIP() << "no complete 2x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
glViewport(0, 0, width, height);
|
||||
|
||||
// Bottom half red, top half blue - via scissored clears, so no shader is involved.
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width, height / 2);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glScissor(0, height / 2, width, height - height / 2);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
const Rgba8 bottom = resolved.At(width / 2, height / 4);
|
||||
const Rgba8 top = resolved.At(width / 2, height - 1 - height / 4);
|
||||
EXPECT_GT(bottom.r, 200) << "the bottom band should be red after the resolve, got rgba("
|
||||
<< static_cast<int>(bottom.r) << ", " << static_cast<int>(bottom.g) << ", "
|
||||
<< static_cast<int>(bottom.b) << ") - blue there means the resolve landed "
|
||||
<< "in the mirrored band";
|
||||
EXPECT_LT(bottom.b, 60);
|
||||
EXPECT_GT(top.b, 200) << "the top band should be blue after the resolve, got rgba("
|
||||
<< static_cast<int>(top.r) << ", " << static_cast<int>(top.g) << ", "
|
||||
<< static_cast<int>(top.b) << ")";
|
||||
EXPECT_LT(top.r, 60);
|
||||
|
||||
glDeleteRenderbuffers(1, &rbo);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The same ordering claim for the path that DOES open a render pass. It passes today (the
|
||||
// render pass folds the clear into its loadOp and pops it), and it is here so a future change
|
||||
// to the pending-clear lifecycle cannot quietly reverse clear and draw.
|
||||
TEST_F(ClearThenReadPixelsScenario, ADrawIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 26, 2) << "the draw did not survive the clear that preceded it";
|
||||
EXPECT_NEAR(centre.g, 51, 2);
|
||||
EXPECT_NEAR(centre.b, 77, 2);
|
||||
|
||||
gl.EndFrame();
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,388 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - gl_ClipDistance ACTUALLY CLIPS, AND ONLY WHERE IT IS ENABLED.
|
||||
//
|
||||
// CapabilityInput::ClipDistance0..7 existed end to end - the GL enum converted to it, the
|
||||
// string converter named it, glEnable(GL_CLIP_DISTANCE0 + i) raised no error - and then
|
||||
// RenderState::SetCapability had no case for it and dropped it into `default: break`. Nothing
|
||||
// was stored, no version was bumped, and neither backend ever heard about it. The shader half
|
||||
// worked all along (SPIRV-Cross emits gl_ClipDistance with a
|
||||
// `#extension GL_EXT_clip_cull_distance : require` that Adreno accepts), so the distances were
|
||||
// computed and then ignored: no clipping ever happened on DirectGLES, which is the whole of
|
||||
// KHR-GLxx.clip_distance.functional. glIsEnabled lied about it too - it returned GL_FALSE
|
||||
// immediately after a successful glEnable.
|
||||
//
|
||||
// The assertions are behavioural, not query-shaped, because a query-only test passes against a
|
||||
// backend that stores the bit and never forwards it. Each case draws one full-viewport triangle
|
||||
// whose clip distance is positive on one side of the viewport and negative on the other, then
|
||||
// checks BOTH sides: the kept side proves the draw happened at all, and the clipped side is the
|
||||
// actual claim. The disabled case is the negative control - the identical shader with the
|
||||
// identical distances and the enable turned off must leave both sides painted, which is what
|
||||
// says the pixels below are being removed by clipping and not by something else.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE IN CI. Of the four cases, only EnableIsObservableThroughIsEnabled is
|
||||
// falsifiable on the software rasterizers every automated lane runs on. llvmpipe and lavapipe
|
||||
// clip by EVERY declared gl_ClipDistance regardless of the enables, so
|
||||
// AnEnabledClipDistanceRemovesTheNegativeHalf goes green there against the broken tree as well,
|
||||
// and the two cases that need real per-distance semantics skip (see
|
||||
// DriverHonoursPerDistanceEnables). What actually pins the behaviour is Adreno, through
|
||||
// KHR-GLxx.clip_distance.functional - whose "without dynamic redeclaration" variants declare all
|
||||
// gl_MaxClipDistances slots and enable only the first N, i.e. exactly the subset semantics these
|
||||
// skipped cases assert. Read a green CI run here as "the state survives the frontend", not as
|
||||
// "clipping is correct"; the second claim is a device claim.
|
||||
//
|
||||
// Every case disables all eight distances on entry rather than assuming they start off:
|
||||
// XfbAfterClipDistanceScenario deliberately leaves one enabled for the rest of the process, and
|
||||
// forwarding the enables is what turned that leftover from inert bookkeeping into live driver
|
||||
// state.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
#ifndef GL_CLIP_DISTANCE0
|
||||
#define GL_CLIP_DISTANCE0 0x3000
|
||||
#endif
|
||||
#ifndef GL_CLIP_DISTANCE1
|
||||
#define GL_CLIP_DISTANCE1 0x3001
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// One clip distance per half of the viewport: distance 0 is positive on the right half
|
||||
// (x > 0 in clip space) and distance 1 is positive on the top half. A vertex shader
|
||||
// producing a full-screen triangle from gl_VertexID, so no buffers are needed.
|
||||
const char* const kVertexSource = R"(#version 400 core
|
||||
out float gl_ClipDistance[2];
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vec2 p = positions[gl_VertexID];
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = p.x;
|
||||
gl_ClipDistance[1] = p.y;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 400 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class ClipDistanceScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(vs);
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
glGetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// Paints the whole viewport red, then draws the clipped triangle in green.
|
||||
void DrawClippedTriangle(GLuint program, GLuint vao) const {
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
static bool IsGreen(const unsigned char* px) {
|
||||
return px[0] < 64 && px[1] > 192;
|
||||
}
|
||||
|
||||
static bool IsRed(const unsigned char* px) {
|
||||
return px[0] > 192 && px[1] < 64;
|
||||
}
|
||||
|
||||
void PixelAt(int x, int y, unsigned char* out) const {
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||
}
|
||||
|
||||
// Never assume the eight start disabled - see the header note about
|
||||
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
||||
static void DisableEveryClipDistance() {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
}
|
||||
|
||||
// True when the driver under this backend actually implements PER-DISTANCE enable
|
||||
// state, i.e. when a written-but-disabled gl_ClipDistance leaves its fragments
|
||||
// alone. Not every stack does, and the difference is not MobileGL's to hide:
|
||||
//
|
||||
// - Adreno's ES driver honours GL_CLIP_DISTANCE0_EXT..7_EXT, which is what makes
|
||||
// KHR-GLxx.clip_distance.functional pass on the device once the enables are
|
||||
// forwarded at all.
|
||||
// - Vulkan has no such state: every clip distance a shader declares is active,
|
||||
// always. DirectVulkan therefore clips by a disabled distance.
|
||||
// - Mesa's llvmpipe ES driver behaves like Vulkan here.
|
||||
//
|
||||
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
|
||||
// non-negative value inside the shader, which makes the enable mask part of the
|
||||
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
|
||||
// cases that need the real semantics gate on this probe and say so when they skip,
|
||||
// rather than being deleted or silently weakened.
|
||||
bool DriverHonoursPerDistanceEnables(GLuint program, GLuint vao) const {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
DrawClippedTriangle(program, vao);
|
||||
unsigned char negativeSide[4] = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 4, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, negativeSide);
|
||||
return IsGreen(negativeSide);
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string ShaderLog(GLuint shader) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
return log.data();
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The state itself: glEnable must be observable through glIsEnabled. This is the cheap half
|
||||
// of the bug - SetCapability's missing case made the query answer GL_FALSE for a capability
|
||||
// that had just been enabled without error.
|
||||
TEST_F(ClipDistanceScenario, EnableIsObservableThroughIsEnabled) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
DisableEveryClipDistance();
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE)
|
||||
<< "glDisable(GL_CLIP_DISTANCE0) is not observable through glIsEnabled";
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_TRUE)
|
||||
<< "glEnable(GL_CLIP_DISTANCE0) raised no error but glIsEnabled still reports it disabled";
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_FALSE)
|
||||
<< "enabling distance 0 must not enable distance 1 - the eight are independent";
|
||||
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_TRUE);
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// Distance 1 is positive by a single pixel at the sampled row, so a stray enable on it
|
||||
// would put the "kept" probe right on the clip boundary.
|
||||
DisableEveryClipDistance();
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "the kept half is not painted (" << int(right[0]) << "," << int(right[1])
|
||||
<< "," << int(right[2]) << ") - the draw itself did not happen, so the clipped "
|
||||
"half below proves nothing";
|
||||
EXPECT_TRUE(IsRed(left)) << "gl_ClipDistance[0] is negative on the left half and GL_CLIP_DISTANCE0 is "
|
||||
"enabled, so those fragments must be clipped away; found ("
|
||||
<< int(left[0]) << "," << int(left[1]) << "," << int(left[2]) << ")";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: the same shader writing the same distances, with the enable off,
|
||||
// must paint both halves. Without this a backend that clipped everything - or one whose
|
||||
// draw simply failed - would pass the case above.
|
||||
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
DisableEveryClipDistance();
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "with every clip distance disabled the whole triangle must survive";
|
||||
const bool driverHonoursEnables = IsGreen(left);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
if (!driverHonoursEnables) {
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by a DISABLED gl_ClipDistance - it does not implement per-distance enable state "
|
||||
"(see DriverHonoursPerDistanceEnables). Emulating GL's semantics there needs shader-side "
|
||||
"masking keyed on the enable mask, which is a separate feature";
|
||||
}
|
||||
}
|
||||
|
||||
// The eight enables are independent: enabling only distance 1 must clip by distance 1 and
|
||||
// leave distance 0 alone. A backend that forwarded "any clip distance enabled" as a single
|
||||
// bit, or that always enables every declared distance (which is what Vulkan does natively),
|
||||
// passes both cases above and fails this one.
|
||||
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
if (!DriverHonoursPerDistanceEnables(program, vao)) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
DisableEveryClipDistance();
|
||||
gl.EndFrame();
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by every declared gl_ClipDistance regardless of the enables, so per-distance "
|
||||
"independence is not observable here";
|
||||
}
|
||||
|
||||
DisableEveryClipDistance();
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Distance 1 is negative on the bottom half, distance 0 on the left half. With only
|
||||
// distance 1 enabled, the bottom-left must survive (distance 0 is off) and the bottom
|
||||
// must not.
|
||||
unsigned char topLeft[4] = {0, 0, 0, 0};
|
||||
unsigned char bottomRight[4] = {0, 0, 0, 0};
|
||||
PixelAt(width / 4, height - 1 - height / 4, topLeft);
|
||||
PixelAt(width - 1 - width / 4, height / 4, bottomRight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(topLeft)) << "gl_ClipDistance[0] is negative here but GL_CLIP_DISTANCE0 is disabled, so "
|
||||
"this fragment must survive";
|
||||
EXPECT_TRUE(IsRed(bottomRight)) << "gl_ClipDistance[1] is negative here and GL_CLIP_DISTANCE1 is enabled, so "
|
||||
"this fragment must be clipped";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||
//
|
||||
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||
//
|
||||
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||
//
|
||||
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||
// them and layerCount counts them; offset.z stays 0.
|
||||
//
|
||||
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||
// scenario per pair is the coverage that matters here.
|
||||
//
|
||||
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||
// slices-landed check and fail this one.
|
||||
//
|
||||
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||
// slice through the same call, so the two texture kinds are read back identically.
|
||||
//
|
||||
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kHeight = 4;
|
||||
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||
// while still leaving untouched slices on either side to assert against.
|
||||
constexpr int kSlices = 6;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||
// make the assertions depend on the framebuffer row order as well.
|
||||
Rgba8 SourceColor(int slice) {
|
||||
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||
}
|
||||
|
||||
Rgba8 DestinationFill(int slice) {
|
||||
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||
}
|
||||
|
||||
class CopyImageLayeredScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||
// simply does not have the call skips instead of failing every case below.
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||
// rather than assumed to be the image's.
|
||||
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(target, texture);
|
||||
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Fill every level, so nothing below can pass by reading a level that was never
|
||||
// written and happened to hold the expected bytes.
|
||||
for (int level = 0; level < levels; ++level) {
|
||||
const int levelWidth = kWidth << (levels - 1 - level);
|
||||
const int levelHeight = kHeight << (levels - 1 - level);
|
||||
const int levelSlices =
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
}
|
||||
glBindTexture(target, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||
// same way.
|
||||
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||
if (m_fbo == 0) {
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||
int dstZ, int depth, const char* what) {
|
||||
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||
const Rgba8 expected =
|
||||
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||
EXPECT_TRUE(actual == expected)
|
||||
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||
// counts carry the depth and extent.depth must stay 1.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||
kWidth, kHeight, kSlices);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||
}
|
||||
|
||||
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 3;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 2;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"array->array, offset layer ranges");
|
||||
}
|
||||
|
||||
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 3;
|
||||
constexpr int kDepth = 3;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||
}
|
||||
|
||||
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||
// this is the only case where the slice count the copy may name is not the image's own -
|
||||
// the bound a layered endpoint is checked against has to come from the level.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kLevel = 1;
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 0;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"3d->3d at mip level 1");
|
||||
}
|
||||
|
||||
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||
}
|
||||
|
||||
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||
// while the source states it as extent.depth from a z offset.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 2;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,209 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.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
|
||||
//
|
||||
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
|
||||
//
|
||||
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
|
||||
// nothing else, and a glCopyImageSubData naming level 1. The answer is
|
||||
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
|
||||
// a valid level"). MobileGL's frontend only checked the level against
|
||||
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
|
||||
// resolved it into a VkImageCopy subresource on a VkImage that was created with
|
||||
// exactly one mip level, and the Adreno driver dereferenced the level it was
|
||||
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
|
||||
// mid-run. A negative case must never do that.
|
||||
//
|
||||
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
|
||||
// rest of this file is what keeps the fix honest. A validator that answered
|
||||
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
|
||||
// scenarios below pin the BOUNDARY rather than the symptom:
|
||||
//
|
||||
// * a texture that really does have two levels must accept a copy at level 1,
|
||||
// * the same texture must still reject level 2,
|
||||
// * and a plain level-0 copy must move pixels, which is checked by reading the
|
||||
// destination back rather than by trusting glGetError.
|
||||
//
|
||||
// Both backends are covered because the fix is in the shared frontend: DirectGLES
|
||||
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
|
||||
// not in MobileGL's, so it never reached the application either) and DirectVulkan
|
||||
// records the copy itself.
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSize = 16;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r, g, b, a;
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
|
||||
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
|
||||
}
|
||||
|
||||
class CopyImageLevelRangeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
DeleteTextures();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteTextures() {
|
||||
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||
m_src = 0;
|
||||
m_dst = 0;
|
||||
}
|
||||
|
||||
// One 16x16 RGBA8 texture with `levelCount` levels defined through
|
||||
// glTexImage2D - the same way the CTS case builds its textures, and
|
||||
// deliberately NOT glTexStorage2D: an immutable allocation would define the
|
||||
// whole chain up front and could not express "level 1 does not exist".
|
||||
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
for (int level = 0; level < levelCount; ++level) {
|
||||
const GLsizei extent = kSize >> level;
|
||||
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
}
|
||||
// What Utils::makeTextureComplete does in the CTS case: the texture is
|
||||
// complete for the levels it actually has, not for a chain it does not.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void MakePair(int levelCount) {
|
||||
DeleteTextures();
|
||||
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
|
||||
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
|
||||
}
|
||||
|
||||
// The call under test, at whatever levels the caller wants, over a 1x1
|
||||
// region so the region check can never be what rejects it.
|
||||
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
|
||||
DrainErrors();
|
||||
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
|
||||
extent, extent, 1);
|
||||
const GLenum error = glGetError();
|
||||
// A second pending error would mean the entry point queued more than one,
|
||||
// and the extra would be handed out at an unrelated call site later.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
|
||||
return error;
|
||||
}
|
||||
|
||||
Rgba8 ReadBackDestinationLevel0() {
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return Rgba8{0, 0, 0, 0};
|
||||
}
|
||||
Rgba8 texel{0, 0, 0, 0};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return texel;
|
||||
}
|
||||
|
||||
GLuint m_src = 0;
|
||||
GLuint m_dst = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// The regression. Level 1 of a texture that has only level 0 is not a level, and
|
||||
// saying so is the whole job: before the fix this reached DirectVulkan, which
|
||||
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
|
||||
// Adreno driver.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
|
||||
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
|
||||
}
|
||||
|
||||
// The negative control that makes the test above falsifiable: the same level
|
||||
// index, on textures that genuinely have it, must be accepted. A validator that
|
||||
// rejected every non-zero level would pass the regression test and fail here.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// And the boundary from the other side: two levels means 0 and 1, not 2.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
|
||||
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
|
||||
}
|
||||
|
||||
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
|
||||
// ordinary case is checked by reading the destination back: the copy has to move
|
||||
// the source's texel, not merely decline to complain.
|
||||
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
|
||||
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - DEPTH/STENCIL READBACK WHEN THE ATTACHMENT IS NOT A PLAIN GL_TEXTURE_2D,
|
||||
// AND THE DEFAULT FRAMEBUFFER'S ADVERTISED DEPTH/STENCIL FORMAT.
|
||||
//
|
||||
// Three shipped defects, all of them invisible to a test that only ever attaches a 2D texture
|
||||
// or only ever asks the default framebuffer for a colour value.
|
||||
//
|
||||
// (1) The ES depth/stencil readback emulation identifies the source format by binding the
|
||||
// attachment's texture NAME to GL_TEXTURE_2D and asking that target for its internal
|
||||
// format. A name whose target is GL_TEXTURE_2D_ARRAY (attached by
|
||||
// glFramebufferTextureLayer) makes the bind answer GL_INVALID_OPERATION and change
|
||||
// nothing - so the query then truthfully describes whatever texture was already on
|
||||
// GL_TEXTURE_2D, which on that path is the emulation's own staging scratch. A wrong
|
||||
// answer that looks like a right one: the staging blit is issued between mismatched
|
||||
// depth formats, ES rejects it, and the read reports nothing at all.
|
||||
//
|
||||
// (2) Adreno answers GL_NONE for GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE on an attachment made
|
||||
// by glFramebufferTexture (a cube map, attached layered) while still reporting its depth
|
||||
// and stencil bits correctly. The emulation took OBJECT_TYPE as the sole witness for "is
|
||||
// there an aspect here at all" and declined the whole read.
|
||||
//
|
||||
// (3) DirectGLES never told the frontend what its default framebuffer's depth/stencil format
|
||||
// actually is, so the placeholder from MG_Impl/Init.cpp - GL_DEPTH32F_STENCIL8 - was what
|
||||
// every attachment query answered, whatever the surface really had. That is not cosmetic:
|
||||
// GL blits depth/stencil only between IDENTICAL formats, so an application that reads
|
||||
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was just told about and
|
||||
// blits gets GL_INVALID_OPERATION - and a rejected glBlitFramebuffer transfers NOTHING,
|
||||
// colour bits included. DirectVulkan has published its real format since the swapchain
|
||||
// work; this is the half that was missing.
|
||||
//
|
||||
// Every case poisons its destination with a value the correct answer cannot be, so "the
|
||||
// backend wrote nothing" fails loudly instead of passing on stale memory. The plain
|
||||
// GL_TEXTURE_2D case at the end is the built-in control: it shares every line of the readback
|
||||
// path with the array and cube cases, so its passing is what says a failure above is about the
|
||||
// attachment's SHAPE and not about depth readback in general.
|
||||
//
|
||||
// The scenario name starts with DepthStencilReadback on purpose - that is the filter the
|
||||
// forced-emulation ctest registration uses (MG_IntegrationTest/CMakeLists.txt), and without
|
||||
// that registration these cases are unfalsifiable on llvmpipe, which accepts the native ES
|
||||
// depth reads that the Adreno device does not have.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
constexpr float kDepthValue = 0.75f;
|
||||
constexpr int kStencilValue = 7;
|
||||
constexpr int kSize = 16;
|
||||
|
||||
class DepthStencilReadbackAttachmentShapeScenario : public ScenarioTest {
|
||||
protected:
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
int ReadStencilAt(int x, int y) const {
|
||||
int stencil = kStencilPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||
return stencil;
|
||||
}
|
||||
|
||||
// Clears the currently bound framebuffer's depth and stencil to the shared
|
||||
// reference values, with both write masks explicitly open (glClear honours them,
|
||||
// and a leftover mask from another scenario in this shared context would look
|
||||
// exactly like the bug under test).
|
||||
void ClearDepthStencil() const {
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearDepth(kDepthValue);
|
||||
glClearStencil(kStencilValue);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
};
|
||||
|
||||
// Fails the calling test if the framebuffer bound at both targets is not complete;
|
||||
// an incomplete framebuffer would make every read below return the poison for a
|
||||
// reason that has nothing to do with what is being tested.
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// (1) A depth slice of a 2D ARRAY texture, attached with glFramebufferTextureLayer.
|
||||
// Pre-fix this read back the poison: the format probe answered with the staging scratch's
|
||||
// GL_DEPTH24_STENCIL8 instead of the array's GL_DEPTH_COMPONENT24, and the mismatched
|
||||
// staging blit was rejected.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAnArrayLayerAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, depthArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kSize, kSize, 4);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
// Layer 2, not layer 0: a backend that silently reads the wrong slice would still
|
||||
// agree with a single-layer texture.
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, 2);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a GL_TEXTURE_2D_ARRAY layer attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (2) A depth cube map, attached whole with glFramebufferTexture - a LAYERED attachment.
|
||||
// Pre-fix the emulation declined outright, because the driver reports GL_NONE for that
|
||||
// attachment's OBJECT_TYPE.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfALayeredCubeAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthCube = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthCube);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCube);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCube, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a layered GL_TEXTURE_CUBE_MAP attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthCube);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// Both aspects of a packed array attachment. The stencil half goes through a different
|
||||
// sampling mode than the depth half, and only the depth half was covered above.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, PackedArrayLayerAttachmentReadsBackBothAspects) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint packedArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &packedArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, packedArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, kSize, kSize, 3);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, packedArray, 0, 1);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
const int stencil = ReadStencilAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << depth;
|
||||
EXPECT_EQ(stencil, kStencilValue)
|
||||
<< "stencil of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << stencil
|
||||
<< (stencil == kStencilPoison ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &packedArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The control: the plain GL_TEXTURE_2D shape, which always worked. If this one ever fails
|
||||
// alongside the three above, the fault is in depth readback generally rather than in how
|
||||
// the attachment's format and presence are discovered.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAPlainTexture2DAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "the control case failed: even a plain GL_TEXTURE_2D depth attachment read back " << depth;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthTex);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (3) The default framebuffer must describe its depth/stencil truthfully enough that a
|
||||
// buffer allocated from that description is blit-compatible with it. This is the exact
|
||||
// sequence KHR-GLxx.framebuffer_blit performs, and the exact reason 22 of its cases died
|
||||
// on DirectGLES: the frontend answered 32-bit float depth for a 24-bit fixed-point
|
||||
// surface, so the renderbuffer the caller allocated could never be blitted to.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DefaultFramebufferDepthStencilFormatIsBlitCompatible) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
GLint depthBits = 0;
|
||||
GLint stencilBits = 0;
|
||||
GLint componentType = GL_UNSIGNED_NORMALIZED;
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
if (depthBits <= 0 || stencilBits <= 0) {
|
||||
GTEST_SKIP() << "this surface has no packed depth/stencil (depth=" << depthBits
|
||||
<< " stencil=" << stencilBits << "); the blit-compatibility contract needs both";
|
||||
}
|
||||
|
||||
// The one sized format the reported description names. Getting here with the wrong
|
||||
// answer is the bug: the two candidates are not interchangeable for a blit.
|
||||
const GLenum reported = (componentType == GL_FLOAT || depthBits > 24) ? GL_DEPTH32F_STENCIL8
|
||||
: GL_DEPTH24_STENCIL8;
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint colorRbo = 0;
|
||||
GLuint depthRbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenRenderbuffers(1, &colorRbo);
|
||||
glGenRenderbuffers(1, &depthRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, colorRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, reported, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
// Put a known depth in the default framebuffer, then blit colour+depth+stencil out of
|
||||
// it into the buffer that its own description asked for.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ClearDepthStencil();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
|
||||
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u)
|
||||
<< "blitting depth/stencil out of the default framebuffer into a buffer allocated from the format "
|
||||
"the default framebuffer itself reported was rejected - the report and the storage disagree";
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
unsigned char color[4] = {0, 0, 0, 0};
|
||||
glReadPixels(width / 2, height / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
|
||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
// The colour bit is the precondition, not the claim: it says this stack can blit out of
|
||||
// its default framebuffer at all, which has nothing to do with depth/stencil formats.
|
||||
// DirectVulkan on a surfaceless pbuffer cannot - the whole call, colour included, is a
|
||||
// no-op there, while the same blit works on a real surface (KHR-GLxx.framebuffer_blit
|
||||
// exercises exactly it and Magma passes 33/33 on device). Skipping keeps the
|
||||
// depth/stencil claim below falsifiable instead of drowning it in an unrelated
|
||||
// harness limitation.
|
||||
if (int(color[1]) <= 192) {
|
||||
// GTEST_SKIP() expands to a return, so the teardown below it would never run and this
|
||||
// scenario would hand the next one a foreign framebuffer plus three leaked objects -
|
||||
// and this is the path DirectVulkan takes on every headless run, not a rare one.
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &colorRbo);
|
||||
glDeleteRenderbuffers(1, &depthRbo);
|
||||
gl.EndFrame();
|
||||
GTEST_SKIP() << "backend " << gl.BackendName() << " on this surface transferred no colour either (green="
|
||||
<< int(color[1])
|
||||
<< "): it cannot blit out of the default framebuffer here, so the depth/stencil half proves "
|
||||
"nothing. The GL-error assertion above still ran, and it is the format contract";
|
||||
}
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth blitted out of the default framebuffer read back " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the blit transferred nothing" : "");
|
||||
EXPECT_EQ(stencil, kStencilValue) << "stencil blitted out of the default framebuffer read back " << stencil;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &colorRbo);
|
||||
glDeleteRenderbuffers(1, &depthRbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,784 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - THE DEPTH/STENCIL READBACK MATRIX: every verb, every source kind.
|
||||
//
|
||||
// DepthStencilReadbackScenario pins the default framebuffer. This file pins the rest of
|
||||
// the surface a depth/stencil read has to cover, because the three verbs and the four
|
||||
// source kinds do NOT share a code path by accident - they share one on purpose, and a
|
||||
// change that quietly serves only one of them is exactly what these assertions catch:
|
||||
//
|
||||
// verbs glReadPixels(GL_DEPTH_COMPONENT | GL_STENCIL_INDEX | GL_DEPTH_STENCIL),
|
||||
// glGetTexImage(GL_DEPTH_STENCIL), glCopyTexImage2D followed by a read
|
||||
// source kinds depth(-stencil) TEXTURE, RENDERBUFFER (not samplable at all),
|
||||
// MULTISAMPLE renderbuffer (needs a resolve first), default framebuffer
|
||||
// formats DEPTH24_STENCIL8, DEPTH32F_STENCIL8, DEPTH_COMPONENT16/24/32F,
|
||||
// STENCIL_INDEX8
|
||||
// client types GL_FLOAT / GL_UNSIGNED_INT / GL_UNSIGNED_SHORT depth, GL_INT /
|
||||
// GL_UNSIGNED_BYTE stencil, both packed GL_DEPTH_STENCIL layouts
|
||||
//
|
||||
// On DirectGLES none of this exists natively - ES has no depth or stencil readback in
|
||||
// core - so every assertion here is really an assertion about the shader-sampling
|
||||
// emulation. The catch is that some ES drivers accept the reads anyway (Mesa does,
|
||||
// Adreno does not), which would make the emulation dead code on the very stack the
|
||||
// headless suite runs on. That is what the second ctest registration is for: the same
|
||||
// scenarios run again with MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1, which takes the
|
||||
// native spellings off the table and leaves only the path the device actually uses.
|
||||
//
|
||||
// Every destination is poisoned with a value the correct answer cannot be, so "the
|
||||
// backend wrote nothing" fails loudly instead of passing on a coincidence - a test that
|
||||
// only checked "no GL error" would pass against a readback that never touched the buffer,
|
||||
// which is precisely how this whole cluster hid for so long.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
constexpr int kWidth = 64;
|
||||
constexpr int kHeight = 48;
|
||||
|
||||
// A depth-stencil pair no clear in these tests produces, packed both ways.
|
||||
constexpr unsigned int kPacked24_8Poison = 0xAAAAAA33u;
|
||||
|
||||
struct D32fS8 {
|
||||
float depth;
|
||||
unsigned int stencil;
|
||||
};
|
||||
|
||||
// Everything a source needs to be read: the framebuffer to bind, plus the objects
|
||||
// to delete afterwards.
|
||||
struct DepthSource {
|
||||
GLuint fbo = 0;
|
||||
GLuint colorTexture = 0;
|
||||
GLuint depthTexture = 0;
|
||||
GLuint depthRenderbuffer = 0;
|
||||
GLuint colorRenderbuffer = 0;
|
||||
};
|
||||
|
||||
void DestroySource(DepthSource& source) {
|
||||
if (source.fbo != 0) glDeleteFramebuffers(1, &source.fbo);
|
||||
if (source.colorTexture != 0) glDeleteTextures(1, &source.colorTexture);
|
||||
if (source.depthTexture != 0) glDeleteTextures(1, &source.depthTexture);
|
||||
if (source.depthRenderbuffer != 0) glDeleteRenderbuffers(1, &source.depthRenderbuffer);
|
||||
if (source.colorRenderbuffer != 0) glDeleteRenderbuffers(1, &source.colorRenderbuffer);
|
||||
source = DepthSource{};
|
||||
}
|
||||
|
||||
GLenum AttachmentPointFor(GLenum internalFormat) {
|
||||
switch (internalFormat) {
|
||||
case GL_DEPTH24_STENCIL8:
|
||||
case GL_DEPTH32F_STENCIL8: return GL_DEPTH_STENCIL_ATTACHMENT;
|
||||
case GL_STENCIL_INDEX8: return GL_STENCIL_ATTACHMENT;
|
||||
default: return GL_DEPTH_ATTACHMENT;
|
||||
}
|
||||
}
|
||||
|
||||
bool FormatHasDepth(GLenum internalFormat) { return internalFormat != GL_STENCIL_INDEX8; }
|
||||
bool FormatHasStencil(GLenum internalFormat) {
|
||||
return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 ||
|
||||
internalFormat == GL_STENCIL_INDEX8;
|
||||
}
|
||||
|
||||
// A framebuffer whose depth/stencil lives in a TEXTURE. The colour attachment is
|
||||
// there so a stencil-only or depth-only framebuffer still has something to size it.
|
||||
DepthSource MakeTextureSource(GLenum internalFormat) {
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenTextures(1, &source.colorTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, source.colorTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.colorTexture, 0);
|
||||
glGenTextures(1, &source.depthTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_TEXTURE_2D,
|
||||
source.depthTexture, 0);
|
||||
return source;
|
||||
}
|
||||
|
||||
// The same, with the depth/stencil in a RENDERBUFFER - which cannot be sampled at
|
||||
// all, so the readback has no choice but to copy it somewhere samplable first.
|
||||
// `samples` > 0 makes it multisample, which additionally needs a resolve.
|
||||
DepthSource MakeRenderbufferSource(GLenum internalFormat, int samples) {
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
if (samples > 0) {
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kWidth, kHeight);
|
||||
} else {
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||
}
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
if (samples > 0) {
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, kWidth, kHeight);
|
||||
} else {
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight);
|
||||
}
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_RENDERBUFFER,
|
||||
source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
return source;
|
||||
}
|
||||
|
||||
// Clears the bound framebuffer's depth and stencil to known values, with the masks
|
||||
// and the scissor explicitly out of the way (a leaked scissor from an earlier
|
||||
// scenario would clip the clear and every assertion after it).
|
||||
void ClearDepthStencil(GLenum internalFormat, float depth, int stencil) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
GLbitfield mask = 0;
|
||||
if (FormatHasDepth(internalFormat)) {
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(depth);
|
||||
mask |= GL_DEPTH_BUFFER_BIT;
|
||||
}
|
||||
if (FormatHasStencil(internalFormat)) {
|
||||
glStencilMask(0xFFu);
|
||||
glClearStencil(stencil);
|
||||
mask |= GL_STENCIL_BUFFER_BIT;
|
||||
}
|
||||
glClear(mask);
|
||||
}
|
||||
|
||||
class DepthStencilReadbackMatrixScenario : public ScenarioTest {
|
||||
protected:
|
||||
// Not every ES driver can render to every depth format (DEPTH_COMPONENT32F and
|
||||
// the multisample counts in particular), and an incomplete framebuffer would
|
||||
// turn a legitimate "this machine cannot host the source" into a spurious
|
||||
// failure about the readback.
|
||||
static bool SourceIsUsable() {
|
||||
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GLenum(GL_FRAMEBUFFER_COMPLETE);
|
||||
}
|
||||
|
||||
static std::vector<float> ReadDepthFloat(int x, int y, int width, int height) {
|
||||
std::vector<float> depth(static_cast<size_t>(width) * height, kDepthPoison);
|
||||
glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data());
|
||||
return depth;
|
||||
}
|
||||
|
||||
static std::vector<int> ReadStencilInt(int x, int y, int width, int height) {
|
||||
std::vector<int> stencil(static_cast<size_t>(width) * height, kStencilPoison);
|
||||
glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_INT, stencil.data());
|
||||
return stencil;
|
||||
}
|
||||
|
||||
// "every value in the region is `expected`" rather than "the middle pixel is":
|
||||
// a staging blit that lands the wrong rectangle, or a conversion pass with a
|
||||
// half-texel offset, still gets the centre right.
|
||||
static void ExpectAllDepth(const std::vector<float>& values, float expected, const char* what) {
|
||||
size_t bad = 0;
|
||||
float worst = expected;
|
||||
for (float value : values) {
|
||||
if (std::fabs(value - expected) > 1.0f / 4096.0f) {
|
||||
if (bad == 0) worst = value;
|
||||
++bad;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||
<< " depth values differ from " << expected << "; first bad value " << worst
|
||||
<< (std::fabs(worst - kDepthPoison) < 1e-6f
|
||||
? " - which is the poison value, so nothing was written at all"
|
||||
: "");
|
||||
}
|
||||
|
||||
static void ExpectAllStencil(const std::vector<int>& values, int expected, const char* what) {
|
||||
size_t bad = 0;
|
||||
int worst = expected;
|
||||
for (int value : values) {
|
||||
if (value != expected) {
|
||||
if (bad == 0) worst = value;
|
||||
++bad;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size()
|
||||
<< " stencil values differ from " << expected << "; first bad value " << worst
|
||||
<< (worst == kStencilPoison
|
||||
? " - which is the poison value, so nothing was written at all"
|
||||
: "");
|
||||
}
|
||||
};
|
||||
|
||||
// ---- glReadPixels across the source kinds -----------------------------------
|
||||
|
||||
struct SourceCase {
|
||||
const char* name;
|
||||
GLenum internalFormat;
|
||||
int samples;
|
||||
bool renderbuffer;
|
||||
};
|
||||
|
||||
const SourceCase kSourceCases[] = {
|
||||
{"texture depth24_stencil8", GL_DEPTH24_STENCIL8, 0, false},
|
||||
{"texture depth32f_stencil8", GL_DEPTH32F_STENCIL8, 0, false},
|
||||
{"texture depth_component16", GL_DEPTH_COMPONENT16, 0, false},
|
||||
{"texture depth_component24", GL_DEPTH_COMPONENT24, 0, false},
|
||||
{"texture depth_component32f", GL_DEPTH_COMPONENT32F, 0, false},
|
||||
{"renderbuffer depth24_stencil8", GL_DEPTH24_STENCIL8, 0, true},
|
||||
{"renderbuffer depth_component24", GL_DEPTH_COMPONENT24, 0, true},
|
||||
{"renderbuffer stencil_index8", GL_STENCIL_INDEX8, 0, true},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, EverySourceKindReadsItsClearBack) {
|
||||
if (!Ready()) return;
|
||||
int exercised = 0;
|
||||
for (const SourceCase& testCase : kSourceCases) {
|
||||
SCOPED_TRACE(testCase.name);
|
||||
DepthSource source = testCase.renderbuffer
|
||||
? MakeRenderbufferSource(testCase.internalFormat, testCase.samples)
|
||||
: MakeTextureSource(testCase.internalFormat);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(source);
|
||||
continue;
|
||||
}
|
||||
FirstGLError(); // the storage calls above may have probed an unsupported combination
|
||||
ClearDepthStencil(testCase.internalFormat, 0.625f, 9);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "clearing the source";
|
||||
|
||||
if (FormatHasDepth(testCase.internalFormat)) {
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_FLOAT)";
|
||||
ExpectAllDepth(depth, 0.625f, testCase.name);
|
||||
}
|
||||
if (FormatHasStencil(testCase.internalFormat)) {
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||
ExpectAllStencil(stencil, 9, testCase.name);
|
||||
}
|
||||
++exercised;
|
||||
DestroySource(source);
|
||||
}
|
||||
// A machine that hosted none of the sources would report a vacuous pass.
|
||||
EXPECT_GE(exercised, 4) << "too few depth/stencil source kinds were usable to call this a matrix";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// Depth and stencil in two SEPARATE objects, with two different formats, on the same
|
||||
// framebuffer. Legal GL, and the shape KHR-GL3x.framebuffer_blit builds when its depth
|
||||
// config and its stencil config are configured independently - so a readback that
|
||||
// describes "the" depth/stencil source as one thing serves whichever aspect it happened
|
||||
// to find first and silently abandons the other. Each aspect has to be staged from its
|
||||
// own attachment, in its own format.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, SeparateDepthAndStencilAttachmentsAreBothReadable) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source;
|
||||
glGenFramebuffers(1, &source.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, source.fbo);
|
||||
glGenRenderbuffers(1, &source.colorRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer);
|
||||
// Depth in a DEPTH_COMPONENT24 renderbuffer...
|
||||
glGenRenderbuffers(1, &source.depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, source.depthRenderbuffer);
|
||||
// ...and stencil in a STENCIL_INDEX8 one of its own.
|
||||
GLuint stencilRenderbuffer = 0;
|
||||
glGenRenderbuffers(1, &stencilRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, stencilRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, kWidth, kHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
if (!SourceIsUsable()) {
|
||||
// Separate depth and stencil images are legal GL but many stacks answer
|
||||
// GL_FRAMEBUFFER_UNSUPPORTED for them; say which, so a skip here is a fact about
|
||||
// the driver rather than an unexplained hole in the matrix.
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||
DestroySource(source);
|
||||
GTEST_SKIP() << "this driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments: "
|
||||
<< "glCheckFramebufferStatus = 0x" << std::hex << status;
|
||||
}
|
||||
FirstGLError();
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glClearDepth(0.3125);
|
||||
glClearStencil(17);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading depth from a separately-attached DEPTH_COMPONENT24";
|
||||
ExpectAllDepth(depth, 0.3125f, "separate depth attachment");
|
||||
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading stencil from a separately-attached STENCIL_INDEX8";
|
||||
ExpectAllStencil(stencil, 17, "separate stencil attachment");
|
||||
|
||||
glDeleteRenderbuffers(1, &stencilRenderbuffer);
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// A multisample source is never read directly - glReadPixels on a multisampled
|
||||
// framebuffer is INVALID_OPERATION in GL as much as in ES, and the state layer says so.
|
||||
// The way multisample depth reaches a reader is a resolve blit into a single-sampled
|
||||
// framebuffer, which is then read; that pair is
|
||||
// KHR-GL3x.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test, and
|
||||
// the assertion here is that the resolved depth arrives intact rather than as the
|
||||
// destination's own clear value.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, AResolvedMultisampleDepthReadsBackFromTheDestination) {
|
||||
if (!Ready()) return;
|
||||
DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(multisampled);
|
||||
GTEST_SKIP() << "this driver cannot host a 4x multisample DEPTH24_STENCIL8 renderbuffer";
|
||||
}
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.875f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// The destination starts at a depth the resolve must overwrite everywhere.
|
||||
DepthSource resolved = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.125f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampled.fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "resolving a multisample depth buffer into a single-sampled one";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, resolved.fbo);
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(depth, 0.875f, "resolved multisample depth");
|
||||
|
||||
DestroySource(resolved);
|
||||
DestroySource(multisampled);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// A read whose rectangle is NOT the whole attachment. The staging copy has to carry
|
||||
// the requested rect (not the origin) and hand back its rows bottom-up, which a
|
||||
// full-extent uniform read is a fixed point of and therefore cannot see.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, ASubRectangleReadsTheRightBandInTheRightOrder) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
|
||||
// Bottom half 0.25, top half 0.75, and the stencil banded the other way round so a
|
||||
// mix-up between the two aspects cannot pass either.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, kWidth, kHeight / 2);
|
||||
glClearDepth(0.25);
|
||||
glClearStencil(11);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
glScissor(0, kHeight / 2, kWidth, kHeight - kHeight / 2);
|
||||
glClearDepth(0.75);
|
||||
glClearStencil(22);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// A rect wholly inside the bottom band, offset from the origin in both axes.
|
||||
const int rectWidth = 8;
|
||||
const int rectHeight = 4;
|
||||
const std::vector<float> bottom = ReadDepthFloat(16, 4, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(bottom, 0.25f, "sub-rect inside the bottom depth band");
|
||||
const std::vector<int> bottomStencil = ReadStencilInt(16, 4, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllStencil(bottomStencil, 11, "sub-rect inside the bottom stencil band");
|
||||
|
||||
// And one wholly inside the top band. Reading the mirrored row would answer 0.25.
|
||||
const std::vector<float> top = ReadDepthFloat(16, kHeight - 4 - rectHeight, rectWidth, rectHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(top, 0.75f, "sub-rect inside the top depth band");
|
||||
|
||||
// A rect that STRADDLES the boundary pins the row order itself: its first rows must
|
||||
// be the bottom band and its last rows the top one.
|
||||
const int straddleHeight = 8;
|
||||
const std::vector<float> straddle =
|
||||
ReadDepthFloat(16, kHeight / 2 - straddleHeight / 2, rectWidth, straddleHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_EQ(straddle.size(), static_cast<size_t>(rectWidth) * straddleHeight);
|
||||
EXPECT_NEAR(straddle[0], 0.25f, 1.0f / 4096.0f)
|
||||
<< "the first row of the returned rect must be its BOTTOM row (GL order), which is in the 0.25 band";
|
||||
EXPECT_NEAR(straddle[straddle.size() - 1], 0.75f, 1.0f / 4096.0f)
|
||||
<< "the last row of the returned rect must be its TOP row, which is in the 0.75 band";
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The packed layouts the packed_depth_stencil family reads its gradients with.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, PackedDepthStencilReadPixelsCarriesBothAspects) {
|
||||
if (!Ready()) return;
|
||||
struct PackedCase {
|
||||
const char* name;
|
||||
GLenum internalFormat;
|
||||
GLenum type;
|
||||
};
|
||||
const PackedCase cases[] = {
|
||||
{"depth24_stencil8 / GL_UNSIGNED_INT_24_8", GL_DEPTH24_STENCIL8, GL_UNSIGNED_INT_24_8},
|
||||
{"depth32f_stencil8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV", GL_DEPTH32F_STENCIL8,
|
||||
GL_FLOAT_32_UNSIGNED_INT_24_8_REV},
|
||||
};
|
||||
int exercised = 0;
|
||||
for (const PackedCase& testCase : cases) {
|
||||
SCOPED_TRACE(testCase.name);
|
||||
DepthSource source = MakeTextureSource(testCase.internalFormat);
|
||||
if (!SourceIsUsable()) {
|
||||
DestroySource(source);
|
||||
continue;
|
||||
}
|
||||
FirstGLError();
|
||||
ClearDepthStencil(testCase.internalFormat, 0.5f, 3);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
if (testCase.type == GL_UNSIGNED_INT_24_8) {
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
const int stencil = static_cast<int>(value & 0xFFu);
|
||||
if (std::fabs(depth - 0.5f) > 0.01f || stencil != 3) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||
<< " packed words carry the wrong depth or stencil (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
} else {
|
||||
std::vector<D32fS8> packed(pixels, D32fS8{kDepthPoison, static_cast<unsigned int>(kStencilPoison)});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (const D32fS8& value : packed) {
|
||||
if (std::fabs(value.depth - 0.5f) > 0.01f || (value.stencil & 0xFFu) != 3u) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels
|
||||
<< " packed pairs carry the wrong depth or stencil (first pair depth "
|
||||
<< packed[0].depth << " stencil " << (packed[0].stencil & 0xFFu) << ")";
|
||||
}
|
||||
++exercised;
|
||||
DestroySource(source);
|
||||
}
|
||||
EXPECT_GE(exercised, 1) << "neither packed depth/stencil format was renderable";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// glGetTexImage reads a TEXTURE, not the bound framebuffer - a different entry point
|
||||
// that has to reach the same machinery. This is verify_get_tex_image's shape.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, GetTexImageReadsAPackedDepthStencilTexture) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.375f, 5);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Read it back through the texture, with the framebuffer that owns it unbound so a
|
||||
// path that secretly read the framebuffer instead would answer from somewhere else.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, source.depthTexture);
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
if (std::fabs(depth - 0.375f) > 0.01f || (value & 0xFFu) != 5u) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << bad << " of " << pixels
|
||||
<< " words from glGetTexImage(GL_DEPTH_STENCIL) are wrong (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroySource(source);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// glCopyTexImage2D out of a depth attachment, then read the copy - verify_copy_tex_image.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, CopyTexImageFromADepthAttachmentSurvivesAReadBack) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.75f, 6);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
GLuint copy = 0;
|
||||
glGenTextures(1, ©);
|
||||
glBindTexture(GL_TEXTURE_2D, copy);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, kWidth, kHeight, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 0, 0, kWidth, kHeight, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glCopyTexImage2D from a depth/stencil attachment";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
std::vector<unsigned int> packed(pixels, kPacked24_8Poison);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
size_t bad = 0;
|
||||
for (unsigned int value : packed) {
|
||||
const float depth = static_cast<float>(value >> 8) / 16777215.0f;
|
||||
if (std::fabs(depth - 0.75f) > 0.01f) ++bad;
|
||||
}
|
||||
EXPECT_EQ(bad, 0u) << bad << " of " << pixels << " copied depth values are wrong (first word 0x" << std::hex
|
||||
<< packed[0] << std::dec << ")";
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, ©);
|
||||
DestroySource(source);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The integer client widths, which are a separate conversion each.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, DepthAndStencilConvertIntoEveryClientWidth) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 200);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const size_t pixels = static_cast<size_t>(kWidth) * kHeight;
|
||||
|
||||
std::vector<unsigned int> depthUint(pixels, 0xDEADBEEFu);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, depthUint.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_INT)";
|
||||
// 0.5 of the full 32-bit range, with room for the source's 24-bit quantisation.
|
||||
EXPECT_NEAR(static_cast<double>(depthUint[0]) / 4294967295.0, 0.5, 0.01)
|
||||
<< "GL_UNSIGNED_INT depth came back as " << depthUint[0];
|
||||
|
||||
std::vector<unsigned short> depthUshort(pixels, 0xBEEFu);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, depthUshort.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT)";
|
||||
EXPECT_NEAR(static_cast<double>(depthUshort[0]) / 65535.0, 0.5, 0.01)
|
||||
<< "GL_UNSIGNED_SHORT depth came back as " << depthUshort[0];
|
||||
|
||||
// A stencil index is written unconverted into whichever width was asked for, so 200
|
||||
// must survive intact in all of them - it is also large enough that a signed byte
|
||||
// would wrap, which is the point of choosing it.
|
||||
std::vector<unsigned char> stencilByte(pixels, static_cast<unsigned char>(kStencilPoison));
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilByte.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE)";
|
||||
EXPECT_EQ(static_cast<int>(stencilByte[0]), 200);
|
||||
|
||||
std::vector<int> stencilInt(pixels, kStencilPoison);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_INT, stencilInt.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)";
|
||||
EXPECT_EQ(stencilInt[0], 200);
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The PACK pixel-store parameters apply to a depth read exactly as they do to a colour
|
||||
// one, and the gap regions they create must be left alone.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, DepthReadbackHonoursThePackPixelStoreParameters) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH_COMPONENT24);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH_COMPONENT24, 0.5f, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const int rectWidth = 4;
|
||||
const int rectHeight = 3;
|
||||
const int rowLength = 8;
|
||||
const int skipPixels = 2;
|
||||
const int skipRows = 1;
|
||||
constexpr float kGap = -7.0f;
|
||||
std::vector<float> destination(static_cast<size_t>(rowLength) * (skipRows + rectHeight) + 16, kGap);
|
||||
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, rowLength);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, skipRows);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glReadPixels(0, 0, rectWidth, rectHeight, GL_DEPTH_COMPONENT, GL_FLOAT, destination.data());
|
||||
const unsigned int readError = FirstGLError();
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
EXPECT_EQ(readError, 0u);
|
||||
|
||||
size_t written = 0;
|
||||
size_t gapsTouched = 0;
|
||||
for (size_t index = 0; index < destination.size(); ++index) {
|
||||
const long row = static_cast<long>(index) / rowLength - skipRows;
|
||||
const long column = static_cast<long>(index) % rowLength - skipPixels;
|
||||
const bool inRect = row >= 0 && row < rectHeight && column >= 0 && column < rectWidth;
|
||||
if (inRect) {
|
||||
if (std::fabs(destination[index] - 0.5f) <= 1.0f / 4096.0f) ++written;
|
||||
} else if (destination[index] != kGap) {
|
||||
++gapsTouched;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(written, static_cast<size_t>(rectWidth) * rectHeight)
|
||||
<< "only " << written << " of " << (rectWidth * rectHeight)
|
||||
<< " destination pixels landed where GL_PACK_ROW_LENGTH/SKIP_* put them";
|
||||
EXPECT_EQ(gapsTouched, 0u) << gapsTouched << " bytes outside the packed rectangle were overwritten";
|
||||
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The readback borrows the application's context for a full-screen pass. Everything it
|
||||
// touches has to come back, or the next draw inherits it - which is how an emulation
|
||||
// that "works" takes the rest of the renderer down with it.
|
||||
TEST_F(DepthStencilReadbackMatrixScenario, ReadbackLeavesNoGLStateBehind) {
|
||||
if (!Ready()) return;
|
||||
DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8);
|
||||
ASSERT_TRUE(SourceIsUsable());
|
||||
FirstGLError();
|
||||
ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 4);
|
||||
|
||||
// A deliberately awkward state: nothing here is what an emulation pass would want,
|
||||
// so anything it forgets to put back shows up below.
|
||||
GLuint scratchTexture = 0;
|
||||
glGenTextures(1, &scratchTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||
glActiveTexture(GL_TEXTURE3);
|
||||
glBindTexture(GL_TEXTURE_2D, scratchTexture);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(3, 5, 7, 11);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilFunc(GL_NOTEQUAL, 0x5, 0x0Fu);
|
||||
glStencilOp(GL_INCR, GL_DECR, GL_INVERT);
|
||||
glStencilMask(0x3Cu);
|
||||
glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
|
||||
glViewport(2, 3, 5, 7);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const std::vector<float> depth = ReadDepthFloat(0, 0, kWidth, kHeight);
|
||||
const std::vector<int> stencil = ReadStencilInt(0, 0, kWidth, kHeight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ExpectAllDepth(depth, 0.5f, "state-preservation case depth");
|
||||
ExpectAllStencil(stencil, 4, "state-preservation case stencil");
|
||||
|
||||
GLint viewport[4] = {0, 0, 0, 0};
|
||||
GLint scissorBox[4] = {0, 0, 0, 0};
|
||||
GLboolean colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||
GLint depthFunc = 0;
|
||||
GLboolean depthMask = GL_TRUE;
|
||||
GLint stencilFunc = 0, stencilRef = 0, stencilValueMask = 0, stencilWriteMask = 0;
|
||||
GLint stencilFail = 0, stencilPassDepthFail = 0, stencilPassDepthPass = 0;
|
||||
GLint activeTexture = 0, boundTexture = 0;
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, scissorBox);
|
||||
glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
|
||||
glGetIntegerv(GL_DEPTH_FUNC, &depthFunc);
|
||||
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
|
||||
glGetIntegerv(GL_STENCIL_FUNC, &stencilFunc);
|
||||
glGetIntegerv(GL_STENCIL_REF, &stencilRef);
|
||||
glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask);
|
||||
glGetIntegerv(GL_STENCIL_WRITEMASK, &stencilWriteMask);
|
||||
glGetIntegerv(GL_STENCIL_FAIL, &stencilFail);
|
||||
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &stencilPassDepthFail);
|
||||
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &stencilPassDepthPass);
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture);
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTexture);
|
||||
|
||||
EXPECT_EQ(viewport[0], 2);
|
||||
EXPECT_EQ(viewport[1], 3);
|
||||
EXPECT_EQ(viewport[2], 5);
|
||||
EXPECT_EQ(viewport[3], 7);
|
||||
EXPECT_EQ(scissorBox[0], 3);
|
||||
EXPECT_EQ(scissorBox[1], 5);
|
||||
EXPECT_EQ(scissorBox[2], 7);
|
||||
EXPECT_EQ(scissorBox[3], 11);
|
||||
EXPECT_EQ(glIsEnabled(GL_SCISSOR_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_CULL_FACE), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_BLEND), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_DEPTH_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(glIsEnabled(GL_STENCIL_TEST), GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(colorMask[0], GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(colorMask[1], GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(colorMask[2], GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(colorMask[3], GLboolean(GL_TRUE));
|
||||
EXPECT_EQ(depthFunc, GLint(GL_GEQUAL));
|
||||
EXPECT_EQ(depthMask, GLboolean(GL_FALSE));
|
||||
EXPECT_EQ(stencilFunc, GLint(GL_NOTEQUAL));
|
||||
EXPECT_EQ(stencilRef, 0x5);
|
||||
EXPECT_EQ(stencilValueMask, 0x0F);
|
||||
EXPECT_EQ(stencilWriteMask, 0x3C);
|
||||
EXPECT_EQ(stencilFail, GLint(GL_INCR));
|
||||
EXPECT_EQ(stencilPassDepthFail, GLint(GL_DECR));
|
||||
EXPECT_EQ(stencilPassDepthPass, GLint(GL_INVERT));
|
||||
EXPECT_EQ(activeTexture, GLint(GL_TEXTURE3));
|
||||
EXPECT_EQ(boundTexture, GLint(scratchTexture))
|
||||
<< "the readback left a scratch texture on the application's texture unit";
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Put the awkward state back so the next scenario in this process starts clean.
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFFu);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
glStencilMask(0xFFFFFFFFu);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glDeleteTextures(1, &scratchTexture);
|
||||
DestroySource(source);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,297 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - glReadPixels OF DEPTH AND STENCIL FROM THE DEFAULT FRAMEBUFFER.
|
||||
//
|
||||
// DirectVulkan's depth/stencil readback used to decline the default framebuffer outright
|
||||
// (`ReadDepthStencilPixels` returned at its first line) because that framebuffer's depth and
|
||||
// stencil "attachments" are placeholder texture objects backing no image - the real one is the
|
||||
// swapchain's depth/stencil twin. Declining meant the call raised no GL error and wrote NOTHING,
|
||||
// so the caller kept whatever its buffer already held.
|
||||
//
|
||||
// That silence is what the framebuffer_blit family trips over. Every one of its cases begins by
|
||||
// clearing the default framebuffer's depth and stencil and reading them straight back as a
|
||||
// sanity check, into a local pre-initialised to 0.2 (depth) and 50 (stencil); an untouched
|
||||
// buffer therefore reports "expected DEPTH[0.25] but got DEPTH[0.2]" and "expected STENCIL[1] but
|
||||
// got STENCIL[50]" - the exact strings in the 15 Magma failures - long before any blit happens.
|
||||
// A test that only checked "no GL error" would pass against the broken path, so every case here
|
||||
// poisons its destination with a value the correct answer cannot be.
|
||||
//
|
||||
// The orientation case is the second half. This renderer stores the default framebuffer
|
||||
// display-side-up and converts GL rects on their way in, so the depth copy needs the same rect
|
||||
// mapping and row re-ordering the colour readback got in the M-1 fix; without them a
|
||||
// vertically-varying depth buffer reads back mirrored, which no full-extent uniform-value test
|
||||
// can see.
|
||||
//
|
||||
// Depth/stencil readback through a USER framebuffer already worked and is asserted here too, as
|
||||
// the built-in control: it shares ReadDepthStencilImageToClient with the default-framebuffer
|
||||
// path, so it is what says a failure is about the default framebuffer specifically.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Values no correct read can produce, so "the backend wrote nothing" fails loudly instead
|
||||
// of passing on whatever happened to be in the variable. These are the CTS's own poison
|
||||
// values, which is why its logs report exactly them.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
|
||||
class DepthStencilReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
// Both backends now answer these reads. DirectGLES has no native ES path for
|
||||
// either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on
|
||||
// both the Adreno device and Mesa's ES), so it stages the attachment into a
|
||||
// scratch depth texture and samples it into a colour target; the assertions below
|
||||
// are the same either way, which is the point.
|
||||
bool BackendReadsDepthStencil() const { return true; }
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
int ReadStencilAt(int x, int y) const {
|
||||
int stencil = kStencilPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||
return stencil;
|
||||
}
|
||||
};
|
||||
|
||||
// A depth buffer whose value depends on the row: bottom half `bottom`, top half `top`.
|
||||
// Built with a scissored clear rather than a draw so the test stays independent of
|
||||
// depth-test and shader behaviour.
|
||||
void ClearDepthInBands(int width, int height, float bottom, float top) {
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width, height / 2);
|
||||
glClearDepth(bottom);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
glScissor(0, height / 2, width, height - height / 2);
|
||||
glClearDepth(top);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
||||
<< " has no depth readback path (ES lacks GL_NV_read_depth); see the packed_depth_stencil "
|
||||
"cluster";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.25);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.25f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of the default framebuffer returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferStencilClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
||||
<< " has no stencil readback path (ES lacks GL_NV_read_stencil); see the "
|
||||
"packed_depth_stencil cluster";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glStencilMask(0xFFu);
|
||||
glClearStencil(3);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
const int centre = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre, 3) << "glReadPixels(GL_STENCIL_INDEX) of the default framebuffer returned " << centre
|
||||
<< (centre == kStencilPoison ? " - the destination was never written at all" : "");
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The orientation half: a depth buffer that varies with the row must read back in GL's
|
||||
// bottom-up order. A full-extent uniform clear is a fixed point of the flip, so only a banded
|
||||
// buffer can tell the two apart.
|
||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthReadbackKeepsTheGLRowOrder) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDepthMask(GL_TRUE);
|
||||
ClearDepthInBands(width, height, /*bottom=*/0.25f, /*top=*/0.75f);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const float bottom = ReadDepthAt(width / 2, height / 4);
|
||||
const float top = ReadDepthAt(width / 2, height - 1 - height / 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(bottom, 0.25f, 1.0f / 4096.0f)
|
||||
<< "GL row " << (height / 4) << " is in the bottom band and was cleared to 0.25, but read back " << bottom
|
||||
<< " (0.75 there means the readback is upside down)";
|
||||
EXPECT_NEAR(top, 0.75f, 1.0f / 4096.0f)
|
||||
<< "GL row " << (height - 1 - height / 4) << " is in the top band and was cleared to 0.75, but read back "
|
||||
<< top << " (0.25 there means the readback is upside down)";
|
||||
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// A depth blit INTO the default framebuffer has to convert its rect out of GL's bottom-origin
|
||||
// space, exactly as the colour blit does. The colour path had that conversion and the
|
||||
// depth path did not, so a scissored depth blit landed in the mirrored band - which is the
|
||||
// whole of KHR-GL*.framebuffer_blit.scissor_blit once the readback above works well enough to
|
||||
// see it (before that the test died on the poison values and never reached the blit).
|
||||
TEST_F(DepthStencilReadbackScenario, AScissoredDepthBlitIntoTheDefaultFramebufferLandsInTheScissorBox) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
// Source: a user framebuffer whose depth is uniformly 0.75.
|
||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenTextures(1, &colorTex);
|
||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.75);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Destination: the default framebuffer, depth 0 everywhere.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glClearDepth(0.0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Blit the whole rect, but scissored to the BOTTOM-LEFT quadrant in GL coordinates.
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, width / 2, height / 2);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const float inside = ReadDepthAt(width / 4, height / 4);
|
||||
const float above = ReadDepthAt(width / 4, height - 1 - height / 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(inside, 0.75f, 1.0f / 4096.0f)
|
||||
<< "GL (" << (width / 4) << ", " << (height / 4) << ") is inside the scissor box and should hold the "
|
||||
<< "blitted 0.75, but read back " << inside;
|
||||
EXPECT_NEAR(above, 0.0f, 1.0f / 4096.0f)
|
||||
<< "GL (" << (width / 4) << ", " << (height - 1 - height / 4)
|
||||
<< ") is ABOVE the scissor box and must still hold the cleared 0.0, but read back " << above
|
||||
<< " (0.75 there means the depth blit landed in the mirrored band)";
|
||||
|
||||
glDeleteTextures(1, &depthTex);
|
||||
glDeleteTextures(1, &colorTex);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The control: the same read against a user framebuffer, which never went through the
|
||||
// declined path. It is what makes a failure above specific to the default framebuffer.
|
||||
TEST_F(DepthStencilReadbackScenario, UserFramebufferDepthClearIsVisibleToReadPixels) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendReadsDepthStencil()) {
|
||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
||||
}
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = 64;
|
||||
const int height = 48;
|
||||
|
||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenTextures(1, &colorTex);
|
||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
||||
GL_UNSIGNED_INT_24_8, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glClearDepth(0.5);
|
||||
glClearStencil(7);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, 0.5f, 1.0f / 4096.0f) << "user-framebuffer depth readback returned " << depth;
|
||||
EXPECT_EQ(stencil, 7) << "user-framebuffer stencil readback returned " << stencil;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteTextures(1, &depthTex);
|
||||
glDeleteTextures(1, &colorTex);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,720 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
||||
//
|
||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
||||
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
||||
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
||||
// shader: `double` compiles and runs everywhere, at float precision.
|
||||
//
|
||||
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
||||
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
||||
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
||||
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
||||
// silently - the uniform simply reads as something else - so the cases below set values
|
||||
// through the API and have the SHADER report what it saw.
|
||||
//
|
||||
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
||||
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
||||
// which is the whole point.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Doubles in every shape the demotion has to handle - a scalar, a vector, a matrix
|
||||
// whose column stride changes, an array whose element stride changes - all reported
|
||||
// through one float SSBO so a single readback says which one moved.
|
||||
constexpr const char* kComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uScalar;
|
||||
uniform dvec3 uVector;
|
||||
uniform dmat4 uMatrix;
|
||||
uniform double uArray[3];
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uScalar);
|
||||
g_out[1] = float(uVector.x);
|
||||
g_out[2] = float(uVector.y);
|
||||
g_out[3] = float(uVector.z);
|
||||
// Column-major [column][row]. Off-diagonal entries catch a column-stride mistake that a
|
||||
// diagonal-only check reads straight past.
|
||||
g_out[4] = float(uMatrix[0][0]);
|
||||
g_out[5] = float(uMatrix[0][3]);
|
||||
g_out[6] = float(uMatrix[3][0]);
|
||||
g_out[7] = float(uMatrix[3][3]);
|
||||
g_out[8] = float(uArray[0]);
|
||||
g_out[9] = float(uArray[1]);
|
||||
g_out[10] = float(uArray[2]);
|
||||
// Arithmetic on doubles, including an implicit float->double conversion and a literal
|
||||
// with the fp64 suffix: this is what an application actually writes, and it is the part
|
||||
// that has to survive the conversion folding.
|
||||
double accumulated = uScalar * 2.0lf + 1.5;
|
||||
g_out[11] = float(accumulated);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kOutputSlots = 12;
|
||||
|
||||
class DoublePrecisionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = CompileComputeProgram(kComputeSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenBuffers(1, &m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
const std::vector<float> zeroes(kOutputSlots, 0.0f);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(float), zeroes.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_shapeOutput != 0) glDeleteBuffers(1, &m_shapeOutput);
|
||||
if (m_shapeProgram != 0) glDeleteProgram(m_shapeProgram);
|
||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int CompileComputeProgram(const char* source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("compute program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
std::vector<float> Dispatch() {
|
||||
glUseProgram(m_program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(kOutputSlots, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
return values;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_output = 0;
|
||||
unsigned int m_shapeProgram = 0;
|
||||
unsigned int m_shapeOutput = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
|
||||
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
|
||||
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
|
||||
// matrices, whose column stride and total size both change when the demotion turns a
|
||||
// 64-bit column into a 32-bit one, and whose members therefore move every uniform
|
||||
// declared after them.
|
||||
//
|
||||
// The shader reports every component separately rather than one pass/fail flag, because
|
||||
// "the readback is wrong" is not a diagnosis: a wrong column stride, a wrong member
|
||||
// offset and a wrong narrowing all fail the same single comparison, and only the
|
||||
// component map says which.
|
||||
// No #version here on purpose: it is handed over as a separate source string, the way
|
||||
// the CTS case hands it over.
|
||||
constexpr const char* kAllDoubleShapesSource = R"(
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double g_0;
|
||||
uniform dvec2 g_1;
|
||||
uniform dvec3 g_2;
|
||||
uniform dvec4 g_3;
|
||||
uniform dmat2 g_4;
|
||||
uniform dmat2x3 g_5;
|
||||
uniform dmat2x4 g_6;
|
||||
uniform dmat3x2 g_7;
|
||||
uniform dmat3 g_8;
|
||||
uniform dmat3x4 g_9;
|
||||
uniform dmat4x2 g_10;
|
||||
uniform dmat4x3 g_11;
|
||||
uniform dmat4 g_12;
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(g_0);
|
||||
for (int i = 0; i < 2; ++i) g_out[1 + i] = float(g_1[i]);
|
||||
for (int i = 0; i < 3; ++i) g_out[3 + i] = float(g_2[i]);
|
||||
for (int i = 0; i < 4; ++i) g_out[6 + i] = float(g_3[i]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 2; ++r) g_out[10 + c * 2 + r] = float(g_4[c][r]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 3; ++r) g_out[14 + c * 3 + r] = float(g_5[c][r]);
|
||||
for (int c = 0; c < 2; ++c) for (int r = 0; r < 4; ++r) g_out[20 + c * 4 + r] = float(g_6[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 2; ++r) g_out[28 + c * 2 + r] = float(g_7[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 3; ++r) g_out[34 + c * 3 + r] = float(g_8[c][r]);
|
||||
for (int c = 0; c < 3; ++c) for (int r = 0; r < 4; ++r) g_out[43 + c * 4 + r] = float(g_9[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 2; ++r) g_out[55 + c * 2 + r] = float(g_10[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 3; ++r) g_out[63 + c * 3 + r] = float(g_11[c][r]);
|
||||
for (int c = 0; c < 4; ++c) for (int r = 0; r < 4; ++r) g_out[75 + c * 4 + r] = float(g_12[c][r]);
|
||||
}
|
||||
)";
|
||||
|
||||
// The values the CTS case sets, spelled the way it spells them - column-major, and small
|
||||
// enough that every one is exact in a float. Nothing here is a precision question; a
|
||||
// component that comes back wrong came back from the wrong bytes.
|
||||
constexpr double kG0 = 1.0;
|
||||
constexpr double kG1[2] = {2.0, 3.0};
|
||||
constexpr double kG2[3] = {4.0, 5.0, 6.0};
|
||||
constexpr double kG3[4] = {7.0, 8.0, 9.0, 10.0};
|
||||
constexpr double kG4[4] = {11.0, 12.0, 13.0, 14.0};
|
||||
constexpr double kG5[6] = {15.0, 16.0, 17.0, 18.0, 19.0, 20.0};
|
||||
constexpr double kG6[8] = {21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0};
|
||||
constexpr double kG7[6] = {29.0, 30.0, 31.0, 32.0, 33.0, 34.0};
|
||||
constexpr double kG8[9] = {35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0};
|
||||
constexpr double kG9[12] = {44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0};
|
||||
constexpr double kG10[8] = {56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0};
|
||||
constexpr double kG11[12] = {63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73.0, 74.0};
|
||||
constexpr double kG12[16] = {75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0,
|
||||
83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0};
|
||||
|
||||
struct DoubleShape {
|
||||
const char* name;
|
||||
int base;
|
||||
int columns; // 1 for the scalar and the vectors
|
||||
int rows; // component count for the scalar and the vectors
|
||||
const double* values;
|
||||
};
|
||||
|
||||
constexpr DoubleShape kDoubleShapes[] = {
|
||||
{"g_0 double", 0, 1, 1, &kG0}, {"g_1 dvec2", 1, 1, 2, kG1},
|
||||
{"g_2 dvec3", 3, 1, 3, kG2}, {"g_3 dvec4", 6, 1, 4, kG3},
|
||||
{"g_4 dmat2", 10, 2, 2, kG4}, {"g_5 dmat2x3", 14, 2, 3, kG5},
|
||||
{"g_6 dmat2x4", 20, 2, 4, kG6}, {"g_7 dmat3x2", 28, 3, 2, kG7},
|
||||
{"g_8 dmat3", 34, 3, 3, kG8}, {"g_9 dmat3x4", 43, 3, 4, kG9},
|
||||
{"g_10 dmat4x2", 55, 4, 2, kG10}, {"g_11 dmat4x3", 63, 4, 3, kG11},
|
||||
{"g_12 dmat4", 75, 4, 4, kG12},
|
||||
};
|
||||
|
||||
constexpr int kAllShapeSlots = 91;
|
||||
|
||||
// The conformance case's own shader, kept verbatim down to the literal suffixes and the
|
||||
// unnamed, unqualified storage block - except that each comparison sets its OWN bit
|
||||
// instead of collapsing all thirteen into one flag. That single flag is the whole reason
|
||||
// the case was unexplained for a wave: it says "something is wrong" and nothing else.
|
||||
//
|
||||
// Verbatim matters here. Reading the components out one at a time (the case above)
|
||||
// passes; whatever fails does so through the shape the conformance case actually
|
||||
// writes - whole-matrix comparison against a constructor, a storage block with no
|
||||
// layout qualifier and no instance name, values reached with constant indices.
|
||||
constexpr const char* kCtsShapedSource = R"(
|
||||
layout(local_size_x = 1) in;
|
||||
buffer Result {
|
||||
int g_result;
|
||||
};
|
||||
uniform double g_0;
|
||||
uniform dvec2 g_1;
|
||||
uniform dvec3 g_2;
|
||||
uniform dvec4 g_3;
|
||||
uniform dmat2 g_4;
|
||||
uniform dmat2x3 g_5;
|
||||
uniform dmat2x4 g_6;
|
||||
uniform dmat3x2 g_7;
|
||||
uniform dmat3 g_8;
|
||||
uniform dmat3x4 g_9;
|
||||
uniform dmat4x2 g_10;
|
||||
uniform dmat4x3 g_11;
|
||||
uniform dmat4 g_12;
|
||||
|
||||
void main() {
|
||||
g_result = 0;
|
||||
|
||||
if (g_0 != 1.0LF) g_result |= 1;
|
||||
if (g_1 != dvec2(2.0LF, 3.0LF)) g_result |= 2;
|
||||
if (g_2 != dvec3(4.0LF, 5.0LF, 6.0LF)) g_result |= 4;
|
||||
if (g_3 != dvec4(7.0LF, 8.0LF, 9.0LF, 10.0LF)) g_result |= 8;
|
||||
|
||||
if (g_4 != dmat2(11.0LF, 12.0LF, 13.0LF, 14.0LF)) g_result |= 16;
|
||||
if (g_5 != dmat2x3(15.0LF, 16.0LF, 17.0LF, 18.0LF, 19.0LF, 20.0LF)) g_result |= 32;
|
||||
if (g_6 != dmat2x4(21.0LF, 22.0LF, 23.0LF, 24.0LF, 25.0LF, 26.0LF, 27.0LF, 28.0LF)) g_result |= 64;
|
||||
|
||||
if (g_7 != dmat3x2(29.0LF, 30.0LF, 31.0LF, 32.0LF, 33.0LF, 34.0LF)) g_result |= 128;
|
||||
if (g_8 != dmat3(35.0LF, 36.0LF, 37.0LF, 38.0LF, 39.0LF, 40.0LF, 41.0LF, 42.0LF, 43.0LF)) g_result |= 256;
|
||||
if (g_9 != dmat3x4(44.0LF, 45.0LF, 46.0LF, 47.0LF, 48.0LF, 49.0LF, 50.0LF, 51.0LF, 52.0LF, 53.0LF, 54.0LF, 55.0LF)) g_result |= 512;
|
||||
|
||||
if (g_10 != dmat4x2(56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0)) g_result |= 1024;
|
||||
if (g_11 != dmat4x3(63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73, 74.0)) g_result |= 2048;
|
||||
if (g_12 != dmat4(75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0)) g_result |= 4096;
|
||||
}
|
||||
)";
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
ASSERT_GE(scalar, 0);
|
||||
// 0.1 has no exact float (or double) representation, so this only passes if the
|
||||
// value really travelled through the demoted slot rather than being read out of
|
||||
// some other four bytes.
|
||||
glUniform1d(scalar, 0.1);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], static_cast<float>(0.1));
|
||||
EXPECT_FLOAT_EQ(values[11], static_cast<float>(static_cast<float>(0.1) * 2.0f + 1.5f))
|
||||
<< "arithmetic on the demoted value, including the folded fp64 literal";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, EveryDoubleShapeLandsInItsOwnSlot) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
const GLint array0 = glGetUniformLocation(m_program, "uArray[0]");
|
||||
const GLint array2 = glGetUniformLocation(m_program, "uArray[2]");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
ASSERT_GE(array0, 0);
|
||||
ASSERT_GE(array2, 0);
|
||||
|
||||
glUniform1d(scalar, 5.0);
|
||||
const GLdouble vectorValue[3] = {11.0, 12.0, 13.0};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
// Column-major, and every entry distinct so a transposed or mis-strided write
|
||||
// cannot land on a value that happens to match.
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
const GLdouble arrayValue[3] = {71.0, 72.0, 73.0};
|
||||
glUniform1dv(array0, 3, arrayValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FLOAT_EQ(values[0], 5.0f) << "scalar double";
|
||||
EXPECT_FLOAT_EQ(values[1], 11.0f) << "dvec3 .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 12.0f) << "dvec3 .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 13.0f) << "dvec3 .z";
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0]";
|
||||
EXPECT_FLOAT_EQ(values[5], 103.0f) << "dmat4 [0][3] - within the first column";
|
||||
EXPECT_FLOAT_EQ(values[6], 112.0f) << "dmat4 [3][0] - column stride";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3]";
|
||||
EXPECT_FLOAT_EQ(values[8], 71.0f) << "double array element 0";
|
||||
EXPECT_FLOAT_EQ(values[9], 72.0f) << "double array element 1 - element stride";
|
||||
EXPECT_FLOAT_EQ(values[10], 73.0f) << "double array element 2";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheTransposeFlagStillTransposes) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(matrix, 0);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_TRUE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
const std::vector<float> values = Dispatch();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
// Transposed, so [column][row] now reads the source's [row][column].
|
||||
EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0] is on the diagonal either way";
|
||||
EXPECT_FLOAT_EQ(values[5], 112.0f) << "dmat4 [0][3] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[6], 103.0f) << "dmat4 [3][0] after transpose";
|
||||
EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3] is on the diagonal either way";
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheUniformIsStillReportedAsADouble) {
|
||||
if (!Ready()) return;
|
||||
// The demotion is an implementation detail of how the value is STORED. What the
|
||||
// shader source declared is what the application asked about, so the reflection
|
||||
// keeps answering GL_DOUBLE* - an application that switches on the type and calls
|
||||
// glUniform*d has to keep working, and it is the glUniform*d path that is correct
|
||||
// for these uniforms.
|
||||
struct Expectation {
|
||||
const char* name;
|
||||
GLenum type;
|
||||
GLint size;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{"uScalar", GL_DOUBLE, 1},
|
||||
{"uVector", GL_DOUBLE_VEC3, 1},
|
||||
{"uMatrix", GL_DOUBLE_MAT4, 1},
|
||||
{"uArray[0]", GL_DOUBLE, 3},
|
||||
};
|
||||
|
||||
GLint activeUniforms = 0;
|
||||
glGetProgramiv(m_program, GL_ACTIVE_UNIFORMS, &activeUniforms);
|
||||
ASSERT_GT(activeUniforms, 0);
|
||||
|
||||
for (const Expectation& expectation : expectations) {
|
||||
bool found = false;
|
||||
for (GLint index = 0; index < activeUniforms; ++index) {
|
||||
char name[128] = {};
|
||||
GLsizei length = 0;
|
||||
GLint size = 0;
|
||||
GLenum type = 0;
|
||||
glGetActiveUniform(m_program, static_cast<GLuint>(index), sizeof(name) - 1, &length, &size,
|
||||
&type, name);
|
||||
if (std::string(name, static_cast<size_t>(length)) != expectation.name) continue;
|
||||
found = true;
|
||||
EXPECT_EQ(type, expectation.type) << expectation.name;
|
||||
EXPECT_EQ(size, expectation.size) << expectation.name;
|
||||
break;
|
||||
}
|
||||
EXPECT_TRUE(found) << "glGetActiveUniform never reported " << expectation.name;
|
||||
}
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, GetUniformdvReadsBackWhatWasStored) {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(m_program);
|
||||
const GLint scalar = glGetUniformLocation(m_program, "uScalar");
|
||||
const GLint vector = glGetUniformLocation(m_program, "uVector");
|
||||
const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
|
||||
ASSERT_GE(scalar, 0);
|
||||
ASSERT_GE(vector, 0);
|
||||
ASSERT_GE(matrix, 0);
|
||||
glUniform1d(scalar, 0.1);
|
||||
const GLdouble vectorValue[3] = {11.5, 12.5, 13.5};
|
||||
glUniform3dv(vector, 1, vectorValue);
|
||||
GLdouble matrixValue[16] = {};
|
||||
for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
|
||||
glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
|
||||
glUseProgram(0);
|
||||
|
||||
// The readback has to undo exactly what the write did - the same std140 column
|
||||
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
||||
// shifted and nothing else in the API would say so.
|
||||
GLdouble readScalar = 0.0;
|
||||
glGetUniformdv(m_program, scalar, &readScalar);
|
||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||
<< "the value is what a float can hold, not the double that was passed in";
|
||||
|
||||
GLdouble readVector[3] = {};
|
||||
glGetUniformdv(m_program, vector, readVector);
|
||||
EXPECT_DOUBLE_EQ(readVector[0], 11.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[1], 12.5);
|
||||
EXPECT_DOUBLE_EQ(readVector[2], 13.5);
|
||||
|
||||
GLdouble readMatrix[16] = {};
|
||||
glGetUniformdv(m_program, matrix, readMatrix);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||
}
|
||||
|
||||
// The float query sees the same storage through the type it is actually stored as.
|
||||
GLfloat readFloat = 0.0f;
|
||||
glGetUniformfv(m_program, scalar, &readFloat);
|
||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, ADoubleUniformKeepsItsDeclaredInitializer) {
|
||||
if (!Ready()) return;
|
||||
// A declared initializer is seeded straight into the uniform shadow at link, and the
|
||||
// seeding used to skip 64-bit floats outright ("no 32-bit shadow encoding") - which
|
||||
// was true before the demotion and silently left every such uniform reading zero.
|
||||
const char* source = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform double uSeeded = 2.5lf;
|
||||
uniform dvec3 uSeededVector = dvec3(4.0lf, 5.0lf, 6.0lf);
|
||||
layout(std430, binding = 0) buffer Output {
|
||||
float g_out[];
|
||||
};
|
||||
void main() {
|
||||
g_out[0] = float(uSeeded);
|
||||
g_out[1] = float(uSeededVector.x);
|
||||
g_out[2] = float(uSeededVector.y);
|
||||
g_out[3] = float(uSeededVector.z);
|
||||
}
|
||||
)";
|
||||
const GLuint program = CompileComputeProgram(source);
|
||||
ASSERT_NE(program, 0u) << m_buildLog;
|
||||
|
||||
glUseProgram(program);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(4, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4 * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
|
||||
EXPECT_FLOAT_EQ(values[0], 2.5f) << "scalar double initializer";
|
||||
EXPECT_FLOAT_EQ(values[1], 4.0f) << "dvec3 initializer .x";
|
||||
EXPECT_FLOAT_EQ(values[2], 5.0f) << "dvec3 initializer .y";
|
||||
EXPECT_FLOAT_EQ(values[3], 6.0f) << "dvec3 initializer .z";
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, EveryDoubleUniformShapeArrivesWhereTheShaderReadsIt) {
|
||||
if (!Ready()) return;
|
||||
// Built the way the CTS case builds it, because every step of that build has been a
|
||||
// bug here at least once: the source arrives as TWO strings (the version directive
|
||||
// and the body), the shader is attached before it has a source and deleted while
|
||||
// still attached, and the program is linked twice.
|
||||
m_shapeProgram = glCreateProgram();
|
||||
ASSERT_NE(m_shapeProgram, 0u);
|
||||
{
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glAttachShader(m_shapeProgram, shader);
|
||||
glDeleteShader(shader);
|
||||
const char* const sources[2] = {"#version 430 core\n", kAllDoubleShapesSource};
|
||||
glShaderSource(shader, 2, sources, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute shader did not compile: " << log;
|
||||
}
|
||||
}
|
||||
glLinkProgram(m_shapeProgram);
|
||||
{
|
||||
GLint linkedOnce = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linkedOnce);
|
||||
if (linkedOnce == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute program did not link: " << log;
|
||||
}
|
||||
}
|
||||
|
||||
glGenBuffers(1, &m_shapeOutput);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
const std::vector<float> zeroes(kAllShapeSlots, 0.0f);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, kAllShapeSlots * sizeof(float), zeroes.data(), GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
|
||||
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||
|
||||
// Pass one sets through glProgramUniform*, pass two through glUniform* after a
|
||||
// re-link - the two entry-point families the CTS case exercises, and two different
|
||||
// routes into the same uniform storage.
|
||||
const auto setWithProgramUniform = [&]() {
|
||||
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||
};
|
||||
// Deliberately does NOT re-issue glUseProgram: the CTS case leaves the program
|
||||
// current across the re-link and writes into it from there, so this is the path
|
||||
// where a re-link has to keep the current program's uniform storage addressable.
|
||||
const auto setWithUniform = [&]() {
|
||||
glUniform1d(location("g_0"), kG0);
|
||||
glUniform2d(location("g_1"), kG1[0], kG1[1]);
|
||||
glUniform3d(location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glUniform4d(location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glUniformMatrix2dv(location("g_4"), 1, GL_FALSE, kG4);
|
||||
glUniformMatrix2x3dv(location("g_5"), 1, GL_FALSE, kG5);
|
||||
glUniformMatrix2x4dv(location("g_6"), 1, GL_FALSE, kG6);
|
||||
glUniformMatrix3x2dv(location("g_7"), 1, GL_FALSE, kG7);
|
||||
glUniformMatrix3dv(location("g_8"), 1, GL_FALSE, kG8);
|
||||
glUniformMatrix3x4dv(location("g_9"), 1, GL_FALSE, kG9);
|
||||
glUniformMatrix4x2dv(location("g_10"), 1, GL_FALSE, kG10);
|
||||
glUniformMatrix4x3dv(location("g_11"), 1, GL_FALSE, kG11);
|
||||
glUniformMatrix4dv(location("g_12"), 1, GL_FALSE, kG12);
|
||||
};
|
||||
|
||||
const auto dispatchAndRead = [&]() {
|
||||
glUseProgram(m_shapeProgram);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
std::vector<float> values(kAllShapeSlots, -1.0f);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), values.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
// The program stays current on purpose - see setWithUniform.
|
||||
return values;
|
||||
};
|
||||
|
||||
const auto expectEverything = [](const std::vector<float>& values, const char* pass) {
|
||||
for (const DoubleShape& shape : kDoubleShapes) {
|
||||
for (int c = 0; c < shape.columns; ++c) {
|
||||
for (int r = 0; r < shape.rows; ++r) {
|
||||
const int component = c * shape.rows + r;
|
||||
EXPECT_FLOAT_EQ(values[shape.base + component],
|
||||
static_cast<float>(shape.values[component]))
|
||||
<< pass << ": " << shape.name << " column " << c << " row " << r;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setWithProgramUniform();
|
||||
expectEverything(dispatchAndRead(), "glProgramUniform*");
|
||||
|
||||
// A re-link zeroes every uniform, so pass two proves its own writes rather than
|
||||
// reading pass one's bytes back.
|
||||
glLinkProgram(m_shapeProgram);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
|
||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), zeroes.data());
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
|
||||
setWithUniform();
|
||||
expectEverything(dispatchAndRead(), "glUniform* after re-link");
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheConformanceUniformShaderAgreesWithEveryValueItWasGiven) {
|
||||
if (!Ready()) return;
|
||||
m_shapeProgram = glCreateProgram();
|
||||
ASSERT_NE(m_shapeProgram, 0u);
|
||||
{
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glAttachShader(m_shapeProgram, shader);
|
||||
glDeleteShader(shader);
|
||||
const char* const sources[2] = {"#version 430 core\n", kCtsShapedSource};
|
||||
glShaderSource(shader, 2, sources, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute shader did not compile: " << log;
|
||||
}
|
||||
}
|
||||
glLinkProgram(m_shapeProgram);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
|
||||
FAIL() << "compute program did not link: " << log;
|
||||
}
|
||||
|
||||
glGenBuffers(1, &m_shapeOutput);
|
||||
const GLint seed = 123;
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(seed), &seed, GL_STATIC_DRAW);
|
||||
|
||||
const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
|
||||
glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
|
||||
glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
|
||||
glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
|
||||
glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
|
||||
glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
|
||||
glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
|
||||
glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
|
||||
glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
|
||||
glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
|
||||
glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
|
||||
glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
|
||||
glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
|
||||
glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
|
||||
|
||||
glUseProgram(m_shapeProgram);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
GLint disagreements = -1;
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(disagreements), &disagreements);
|
||||
for (int bit = 0; bit < 13; ++bit) {
|
||||
EXPECT_EQ(disagreements & (1 << bit), 0)
|
||||
<< kDoubleShapes[bit].name << " did not compare equal to the value it was given";
|
||||
}
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
|
||||
if (!Ready()) return;
|
||||
// The shader above compiled, linked and ran without the extension string, which is
|
||||
// the point: an application does not need GL_ARB_gpu_shader_fp64 advertised to USE
|
||||
// doubles here. What the string additionally promises is 64-bit precision, and that
|
||||
// is the one thing the demotion cannot deliver - so it stays off unless
|
||||
// MOBILEGL_ADVERTISE_FP64 asks for it, and an application that branches on the
|
||||
// string keeps taking its float path.
|
||||
GLint extensionCount = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
||||
ASSERT_GT(extensionCount, 0);
|
||||
bool advertised = false;
|
||||
for (GLint i = 0; i < extensionCount; ++i) {
|
||||
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
||||
if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true;
|
||||
}
|
||||
EXPECT_FALSE(advertised);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
||||
if (!Ready()) return;
|
||||
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
||||
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
||||
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
||||
// drawn as garbage; the matching POST row says the same thing at startup.
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
|
||||
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,348 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DrawParametersScenario.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
|
||||
//
|
||||
// gl_BaseVertex / gl_BaseInstance / gl_DrawID (GL_ARB_shader_draw_parameters),
|
||||
// read straight out of the shader that a draw command produced.
|
||||
//
|
||||
// Neither backend has these builtins for free, and each is wrong in its own way
|
||||
// when nobody watches:
|
||||
//
|
||||
// * DirectVulkan HAS a BaseVertex builtin, but Vulkan's carries the draw's
|
||||
// firstVertex on a NON-INDEXED draw where GL's is defined to be zero ("the
|
||||
// value passed to the baseVertex parameter, or zero for a command with no
|
||||
// such parameter"). Only the indexed meaning of the two agrees. Every
|
||||
// DrawArrays form therefore takes the ZeroBaseVertex program variant.
|
||||
// * DirectGLES has no such builtins at all: ESSL knows none of them, so the
|
||||
// transpiler demotes each one to a uniform the draw paths feed. A uniform
|
||||
// nobody writes keeps whatever the previous draw left in it - which is what
|
||||
// made gl_BaseVertex report a stale base vertex, and what made
|
||||
// gl_BaseInstance read an unbound storage buffer on a plain glDrawArrays.
|
||||
//
|
||||
// The shader paints the three values, so a draw that carries the wrong ones
|
||||
// paints the wrong colour rather than merely disagreeing with an expectation
|
||||
// somewhere. The framebuffer is cleared to WHITE and no case expects 255 in any
|
||||
// channel, so "the draw did not happen" can never be mistaken for a pass.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// #version 450: glslang only declares the ARB builtins from 440 up.
|
||||
//
|
||||
// Each value is painted at 8 units per count, not 1: the errors these builtins
|
||||
// actually have are OFF BY ONE (a sub-draw that never got its own gl_DrawID reads
|
||||
// the previous one's, a base vertex that arrives one command late), and at one unit
|
||||
// per count no readback tolerance can tell those from rounding.
|
||||
//
|
||||
// And biased by two counts, so that ZERO is not the clamp floor. Five of these cases
|
||||
// expect zero, and an unbiased encoding would let every negative value - the shape a
|
||||
// sign or rebase mistake produces - clamp to the same black and pass.
|
||||
constexpr const char* kVertexSource = R"(#version 450 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
layout(location = 0) in vec2 aPos;
|
||||
flat out vec3 vParams;
|
||||
void main() {
|
||||
vParams = (vec3(gl_BaseVertexARB, gl_BaseInstanceARB, gl_DrawIDARB) * 8.0 + 16.0) / 255.0;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 450 core
|
||||
flat in vec3 vParams;
|
||||
out vec4 oColor;
|
||||
void main() {
|
||||
oColor = vec4(vParams, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
};
|
||||
|
||||
// 3 dummy vertices, then the left half of the viewport as two triangles,
|
||||
// then the right half. Nothing here is symmetric by accident:
|
||||
//
|
||||
// * the padding makes a draw that ignores `first` / baseVertex paint a
|
||||
// degenerate triangle (i.e. nothing) instead of the right picture;
|
||||
// * the two halves let one multi-draw show TWO different gl_DrawID
|
||||
// values in one readback.
|
||||
//
|
||||
// Indices 3..14 together cover the whole viewport, which is what the
|
||||
// single-draw cases use.
|
||||
constexpr int kPad = 3;
|
||||
constexpr int kLeftFirst = kPad; // 3
|
||||
constexpr int kRightFirst = kPad + 6; // 9
|
||||
constexpr int kHalfCount = 6;
|
||||
|
||||
std::vector<Vertex> SceneVertices() {
|
||||
std::vector<Vertex> vertices(static_cast<std::size_t>(kPad), Vertex{0.0f, 0.0f});
|
||||
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
|
||||
for (const auto& half : bounds) {
|
||||
const float x0 = half[0];
|
||||
const float x1 = half[1];
|
||||
vertices.push_back({x0, -1.0f});
|
||||
vertices.push_back({x1, -1.0f});
|
||||
vertices.push_back({x1, 1.0f});
|
||||
vertices.push_back({x0, -1.0f});
|
||||
vertices.push_back({x1, 1.0f});
|
||||
vertices.push_back({x0, 1.0f});
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
// GL's DrawArraysIndirectCommand / DrawElementsIndirectCommand, spelled out
|
||||
// so a test can write one without depending on a GL header's struct.
|
||||
struct ArraysCommand {
|
||||
std::uint32_t count, instanceCount, first, baseInstance;
|
||||
};
|
||||
struct ElementsCommand {
|
||||
std::uint32_t count, instanceCount, firstIndex;
|
||||
std::int32_t baseVertex;
|
||||
std::uint32_t baseInstance;
|
||||
};
|
||||
|
||||
class DrawParametersScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
|
||||
const std::vector<Vertex> vertices = SceneVertices();
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
|
||||
vertices.data(), GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (GLuint* buffer : {&m_ebo, &m_indirect, &m_parameter, &m_vbo}) {
|
||||
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
||||
*buffer = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void FillBuffer(GLuint& name, GLenum target, const std::vector<T>& data) {
|
||||
if (name == 0) glGenBuffers(1, &name);
|
||||
glBindBuffer(target, name);
|
||||
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_STATIC_DRAW);
|
||||
}
|
||||
|
||||
// Clears to white, runs `draw` and reads the frame back.
|
||||
template <typename DrawFn>
|
||||
Image Render(DrawFn&& draw) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
draw();
|
||||
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||
}
|
||||
|
||||
// The three builtins as the shader saw them, at a point in one half of
|
||||
// the viewport. `half` is 0 for the left half and 1 for the right.
|
||||
struct DrawParams {
|
||||
int baseVertex = -1, baseInstance = -1, drawId = -1;
|
||||
};
|
||||
// Decodes the biased 8-units-per-count encoding back to the integer the
|
||||
// shader saw. Rounding to the nearest step absorbs any UNORM slop; adjacent
|
||||
// values stay eight units apart, so an off-by-one still reads as one, and a
|
||||
// negative value lands below the bias and decodes negative rather than
|
||||
// clamping into a legitimate zero.
|
||||
static DrawParams ParamsAt(const Image& image, int half) {
|
||||
const int x = image.Width() * (1 + 2 * half) / 4;
|
||||
const Rgba8 pixel = image.At(x, image.Height() / 2);
|
||||
const auto decode = [](std::uint8_t channel) {
|
||||
return (static_cast<int>(channel) - 16 + 4) / 8;
|
||||
};
|
||||
return {decode(pixel.r), decode(pixel.g), decode(pixel.b)};
|
||||
}
|
||||
|
||||
static void ExpectParams(const Image& image, int half, const DrawParams& expected,
|
||||
const std::string& what) {
|
||||
const DrawParams actual = ParamsAt(image, half);
|
||||
EXPECT_EQ(actual.baseVertex, expected.baseVertex)
|
||||
<< what << ": gl_BaseVertex (half " << half << ")";
|
||||
EXPECT_EQ(actual.baseInstance, expected.baseInstance)
|
||||
<< what << ": gl_BaseInstance (half " << half << ")";
|
||||
EXPECT_EQ(actual.drawId, expected.drawId) << what << ": gl_DrawID (half " << half << ")";
|
||||
}
|
||||
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_indirect = 0;
|
||||
GLuint m_parameter = 0;
|
||||
};
|
||||
|
||||
// ---- the non-indexed forms: gl_BaseVertex is zero, `first` or not ----
|
||||
|
||||
// Vulkan's BaseVertex would answer 3 here (the draw's firstVertex); GL's
|
||||
// must answer 0, because glDrawArrays has no baseVertex parameter at all.
|
||||
TEST_F(DrawParametersScenario, DrawArraysReportsAZeroBaseVertexDespiteItsFirst) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] { glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount); });
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glDrawArrays(first=3)");
|
||||
ExpectParams(image, 1, {0, 0, 0}, "glDrawArrays(first=3)");
|
||||
}
|
||||
|
||||
TEST_F(DrawParametersScenario, DrawArraysInstancedBaseInstanceReportsItsBaseInstance) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] {
|
||||
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 5);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 5, 0}, "glDrawArraysInstancedBaseInstance(baseInstance=5)");
|
||||
}
|
||||
|
||||
// The base instance of one draw must not survive into the next one. This is
|
||||
// the shape that broke on DirectGLES: the emulation uniform is per-program
|
||||
// state, so a draw that never writes it inherits the last writer's value.
|
||||
TEST_F(DrawParametersScenario, APlainDrawAfterABaseInstancedOneSeesZeroAgain) {
|
||||
if (!Ready()) return;
|
||||
const Image image = Render([&] {
|
||||
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 7);
|
||||
glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "plain glDrawArrays after a base-instanced draw");
|
||||
}
|
||||
|
||||
// ---- the indexed forms: gl_BaseVertex IS the base vertex ----
|
||||
|
||||
TEST_F(DrawParametersScenario, DrawElementsBaseVertexReportsItsBaseVertex) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(0), kLeftFirst);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
||||
ExpectParams(image, 1, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
||||
}
|
||||
|
||||
// ... and is zero again for the command that has none, including after one
|
||||
// that did: the same leak the base instance has, on the other builtin. The
|
||||
// preceding draw MUST carry a non-zero base vertex or this case proves nothing -
|
||||
// one index run reaches the geometry through the base vertex, the second through
|
||||
// its own indices, so the two draws paint the same picture with different
|
||||
// gl_BaseVertex and only the second one's value survives in the framebuffer.
|
||||
TEST_F(DrawParametersScenario, DrawElementsAfterABaseVertexDrawReportsZeroAgain) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
||||
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i + kLeftFirst);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
const auto rebasedRun = reinterpret_cast<const void*>(2 * kHalfCount * sizeof(std::uint32_t));
|
||||
|
||||
const Image image = Render([&] {
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(0), kLeftFirst);
|
||||
glDrawElements(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT, rebasedRun);
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
||||
ExpectParams(image, 1, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
||||
}
|
||||
|
||||
// ---- the multi-draw forms: one gl_DrawID per sub-draw ----
|
||||
|
||||
TEST_F(DrawParametersScenario, MultiDrawArraysNumbersItsSubDraws) {
|
||||
if (!Ready()) return;
|
||||
const GLint firsts[2] = {kLeftFirst, kRightFirst};
|
||||
const GLsizei counts[2] = {kHalfCount, kHalfCount};
|
||||
|
||||
const Image image = Render([&] { glMultiDrawArrays(GL_TRIANGLES, firsts, counts, 2); });
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "glMultiDrawArrays sub-draw 0");
|
||||
ExpectParams(image, 1, {0, 0, 1}, "glMultiDrawArrays sub-draw 1");
|
||||
}
|
||||
|
||||
// Every field of an indexed indirect command at once: its own gl_DrawID, the
|
||||
// baseVertex word (which the CPU reads out of the command) and the
|
||||
// baseInstance word (which DirectGLES reads through a storage-buffer view of
|
||||
// the very same buffer).
|
||||
TEST_F(DrawParametersScenario, MultiDrawElementsIndirectCarriesEveryCommandsParameters) {
|
||||
if (!Ready()) return;
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t i = 0; i < kHalfCount; ++i) indices.push_back(i);
|
||||
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||
|
||||
const std::vector<ElementsCommand> commands = {
|
||||
{kHalfCount, 1, 0, kLeftFirst, 0},
|
||||
{kHalfCount, 1, 0, kRightFirst, 4},
|
||||
};
|
||||
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, reinterpret_cast<const void*>(0), 2,
|
||||
sizeof(ElementsCommand));
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "indirect command 0");
|
||||
ExpectParams(image, 1, {kRightFirst, 4, 1}, "indirect command 1");
|
||||
}
|
||||
|
||||
// glMultiDrawArraysIndirectCount was missing from the DirectGLES backend
|
||||
// table entirely, so the frontend answered INVALID_OPERATION for every call
|
||||
// while GL_ARB_indirect_parameters was advertised. The parameter buffer here
|
||||
// holds a count SMALLER than maxdrawcount, so a path that ignores it draws a
|
||||
// third command over the top of the second and changes the right half.
|
||||
TEST_F(DrawParametersScenario, MultiDrawArraysIndirectCountObeysItsParameterBuffer) {
|
||||
if (!Ready()) return;
|
||||
const std::vector<ArraysCommand> commands = {
|
||||
{kHalfCount, 1, kLeftFirst, 0},
|
||||
{kHalfCount, 1, kRightFirst, 6},
|
||||
{kHalfCount, 1, kRightFirst, 9},
|
||||
};
|
||||
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
||||
const std::vector<std::uint32_t> parameters = {2};
|
||||
FillBuffer(m_parameter, GL_PARAMETER_BUFFER, parameters);
|
||||
|
||||
const Image image = Render([&] {
|
||||
glMultiDrawArraysIndirectCount(GL_TRIANGLES, reinterpret_cast<const void*>(0), 0, 3,
|
||||
sizeof(ArraysCommand));
|
||||
});
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
ExpectParams(image, 0, {0, 0, 0}, "counted indirect command 0");
|
||||
ExpectParams(image, 1, {0, 6, 1}, "counted indirect command 1");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,139 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FragCoordOriginScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - gl_FragCoord ON THE DEFAULT FRAMEBUFFER CARRIES GL'S WINDOW ORIGIN.
|
||||
//
|
||||
// GL measures gl_FragCoord.y from the BOTTOM of the window. Vulkan's gl_FragCoord.y is the
|
||||
// framebuffer ROW being written, and DirectVulkan stores the default framebuffer display-side-up
|
||||
// (compensating for vertices by negating gl_Position.y), so a fragment's reported Y there was
|
||||
// `height - y_GL` - flipped, and for a viewport that does not span the full height, outside the
|
||||
// range GL promises entirely. GL CTS
|
||||
// `KHR-GL42.shader_image_load_store.basic-{allTargets-atomic,glsl-earlyFragTests,glsl-misc}`
|
||||
// caught it: each sets a small viewport at GL y=0 and does
|
||||
// `imageStore(image, ivec2(gl_FragCoord.xy), ...)` into an image exactly that size, so on a
|
||||
// 256-tall surface every store addressed rows 224..255 of a 32-row image and was dropped.
|
||||
//
|
||||
// The shader here paints each row with its own GL window Y, which is the whole claim in one
|
||||
// value: row j of the readback must be j, for a full-height viewport and for a half-height one
|
||||
// (the case where a flip and an offset can no longer hide each other). DirectGLES is the
|
||||
// built-in control - a native GL driver gets this right by construction, so a failure there
|
||||
// would mean the test, not the backend.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// floor(gl_FragCoord.y) is the fragment's window row; 1/255 steps survive an RGBA8
|
||||
// round trip exactly, so the readback byte IS the row the shader believes it is on.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(floor(gl_FragCoord.y) / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class FragCoordOriginScenario : public ScenarioTest {};
|
||||
|
||||
// A quad covering the whole viewport, drawn with attribute 0 = aPos.
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
// Paints `viewportHeight` rows starting at GL y=0 and returns the red byte of each row.
|
||||
std::vector<int> RowsPaintedWithTheirOwnWindowY(unsigned int program, int width, int viewportHeight) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, viewportHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
|
||||
const Image image = ReadPixelsRect(0, 0, width, viewportHeight);
|
||||
std::vector<int> rows;
|
||||
rows.reserve(static_cast<std::size_t>(viewportHeight));
|
||||
for (int y = 0; y < viewportHeight; ++y) {
|
||||
rows.push_back(image.At(width / 2, y).r);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
::testing::AssertionResult RowsAreTheirOwnIndex(const std::vector<int>& rows, const char* when) {
|
||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||
if (rows[y] != static_cast<int>(y)) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< when << ": GL window row " << y << " reported gl_FragCoord.y = " << rows[y]
|
||||
<< " (expected " << y << "). Rows 0.." << (rows.size() - 1) << " read back as ["
|
||||
<< rows.front() << " .. " << rows.back() << "].";
|
||||
}
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(FragCoordOriginScenario, DefaultFramebufferFragCoordCountsFromTheBottom) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
// 1/255 steps only stay distinguishable while the row index fits in a byte.
|
||||
const int width = gl.Width();
|
||||
const int fullHeight = std::min(gl.Height(), 256);
|
||||
ASSERT_GE(fullHeight, 8) << "the harness surface is too small to tell rows apart";
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// Full height first: this one passed even before the fix (a flip alone maps the row set
|
||||
// onto itself), so it is the control that the shader and the readback agree at all.
|
||||
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, fullHeight),
|
||||
"full-height viewport"));
|
||||
|
||||
// Half height at GL y=0: the case the CTS failures were made of. A backend that reports
|
||||
// the stored row here answers `height - y` for every row - off the bottom of the range,
|
||||
// not merely reversed within it.
|
||||
const int halfHeight = fullHeight / 2;
|
||||
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, halfHeight),
|
||||
"half-height viewport at GL y=0"));
|
||||
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,227 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - DYNAMICALLY INDEXED FRAGMENT OUTPUT ARRAYS, on a live driver.
|
||||
//
|
||||
// The bug: GLSL ES requires a *constant integral expression* to index a fragment output array
|
||||
// (GLSL ES 3.00 4.3.6); SPIR-V has no such rule. A shader that writes `coeff[i]` from a loop
|
||||
// therefore travels through glslang and SPIRV-Cross intact and lands on the ES driver as ESSL it
|
||||
// refuses outright - "array indexes for fragment outputs must be constant integral expressions".
|
||||
// The program links nothing and every draw that uses it becomes a silent no-op. That is the whole
|
||||
// of improved-transparency-minecraft-26.3 on the Android DirectGLES lane: Minecraft 26.3's OIT
|
||||
// coefficient shader has exactly this shape, and losing it empties the entire translucent layer
|
||||
// (clouds and water) while the opaque geometry stays pixel-exact.
|
||||
//
|
||||
// WHY THIS SCENARIO EXISTS RATHER THAN A UNIT TEST. The unit tests in MG_Test/Program (see
|
||||
// ProgramUtilTest, LoopDerivedFragmentOutputIndexFoldsToConstantIndices and its
|
||||
// genuinely-dynamic sibling) prove the SPIR-V comes out with constant indices, validates, and
|
||||
// decompiles to ESSL with only literal indices. What they cannot prove is that a real driver
|
||||
// then ACCEPTS and RUNS it - and acceptance is the whole failure mode, because Mesa accepts the
|
||||
// illegal form too. Only a live glCompileShader/glLinkProgram followed by a draw can tell the two
|
||||
// apart, and only reading the pixels back can tell "linked" from "wrote the right attachment".
|
||||
//
|
||||
// Both backends run this: on DirectVulkan the original module is already legal (the legalization
|
||||
// is DirectGLES-only, deliberately), so this doubles as the check that the two backends agree
|
||||
// about what such a shader means.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() {
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The Minecraft 26.3 OIT coefficient shape: both the attachment index and the component
|
||||
// index come from loop counters, so nothing but the loop bounds decides where each value
|
||||
// lands. Attachment 0 gets (0.0, 0.1, 0.2, 0.3) and attachment 1 gets (0.5, 0.6, 0.7, 0.8) -
|
||||
// values that are only correct if the two indices were folded to the RIGHT constants, not
|
||||
// merely to some constant.
|
||||
constexpr const char* kLoopIndexedFS = R"(#version 330 core
|
||||
out vec4 coeff[2];
|
||||
void main() {
|
||||
for (int attachmentIndex = 0; attachmentIndex < 2; ++attachmentIndex) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
coeff[attachmentIndex][i] = float(attachmentIndex) * 0.5 + float(i) * 0.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// No loop can fold this one: the index arrives in a uniform. It exercises the fallback
|
||||
// lowering (a switch over the array range for the write, constant-indexed loads and a
|
||||
// select for the read) and it checks the untargeted attachment is left ALONE, which a
|
||||
// lowering that wrote every element unconditionally would break.
|
||||
constexpr const char* kUniformIndexedFS = R"(#version 330 core
|
||||
uniform int uTarget;
|
||||
out vec4 coeff[2];
|
||||
void main() {
|
||||
coeff[0] = vec4(0.25, 0.25, 0.25, 1.0);
|
||||
coeff[1] = vec4(0.75, 0.75, 0.75, 1.0);
|
||||
coeff[uTarget] = coeff[uTarget] + vec4(0.25, 0.0, 0.0, 0.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr int kSize = 8;
|
||||
|
||||
class FragmentOutputArrayIndexScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
glGenTextures(1, &m_color[i]);
|
||||
glBindTexture(GL_TEXTURE_2D, m_color[i]);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D,
|
||||
m_color[i], 0);
|
||||
}
|
||||
const GLenum drawBuffers[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
|
||||
glDrawBuffers(2, drawBuffers);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
|
||||
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
glDeleteTextures(2, m_color);
|
||||
glDeleteBuffers(1, &m_vbo);
|
||||
glDeleteVertexArrays(1, &m_vao);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// Clears both attachments to a colour no shader below writes, so an attachment that
|
||||
// was never written reads back as the sentinel rather than as a plausible value.
|
||||
void ClearToSentinel() {
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
std::vector<float> ReadAttachment(int index) {
|
||||
std::vector<unsigned char> bytes(static_cast<std::size_t>(kSize) * kSize * 4, 0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0 + index);
|
||||
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, bytes.data());
|
||||
std::vector<float> centre(4, -1.0f);
|
||||
// The middle pixel: the quad covers the whole target, so every pixel is the same,
|
||||
// and the middle one cannot be a rasterization edge case.
|
||||
const std::size_t offset = (static_cast<std::size_t>(kSize / 2) * kSize + kSize / 2) * 4;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
centre[static_cast<std::size_t>(i)] = static_cast<float>(bytes[offset + i]) / 255.0f;
|
||||
}
|
||||
return centre;
|
||||
}
|
||||
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_color[2] = {0, 0};
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
};
|
||||
|
||||
// The gate for the whole defect: before the legalization this program did not link on a
|
||||
// strict ES driver (ANGLE), so the draw wrote nothing and BOTH attachments kept the
|
||||
// sentinel. Now each attachment must carry the value its loop iteration produced.
|
||||
TEST_F(FragmentOutputArrayIndexScenario, LoopIndexedOutputArrayWritesEveryAttachment) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kVS, kLoopIndexedFS, &error);
|
||||
ASSERT_NE(program, 0u) << "a loop-indexed fragment output array must compile and link: "
|
||||
<< error;
|
||||
|
||||
ClearToSentinel();
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
const std::vector<float> first = ReadAttachment(0);
|
||||
EXPECT_NEAR(first[0], 0.0f, 0.02f) << "attachment 0 red";
|
||||
EXPECT_NEAR(first[1], 0.1f, 0.02f) << "attachment 0 green";
|
||||
EXPECT_NEAR(first[2], 0.2f, 0.02f)
|
||||
<< "attachment 0 blue - a sentinel 1.0 here means the draw never ran";
|
||||
EXPECT_NEAR(first[3], 0.3f, 0.02f) << "attachment 0 alpha";
|
||||
|
||||
const std::vector<float> second = ReadAttachment(1);
|
||||
EXPECT_NEAR(second[0], 0.5f, 0.02f)
|
||||
<< "attachment 1 red - the second loop iteration must reach the second draw buffer";
|
||||
EXPECT_NEAR(second[1], 0.6f, 0.02f) << "attachment 1 green";
|
||||
EXPECT_NEAR(second[2], 0.7f, 0.02f) << "attachment 1 blue";
|
||||
EXPECT_NEAR(second[3], 0.8f, 0.02f) << "attachment 1 alpha";
|
||||
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The fallback half, on a live driver, for both values of the uniform: the targeted
|
||||
// attachment is read, incremented and written back; the other one keeps exactly what the
|
||||
// constant-indexed store put there.
|
||||
TEST_F(FragmentOutputArrayIndexScenario, UniformIndexedOutputArrayWritesOnlyTheSelectedAttachment) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kVS, kUniformIndexedFS, &error);
|
||||
ASSERT_NE(program, 0u) << "a uniform-indexed fragment output array must compile and link: "
|
||||
<< error;
|
||||
const GLint targetLocation = glGetUniformLocation(program, "uTarget");
|
||||
ASSERT_GE(targetLocation, 0);
|
||||
glUseProgram(program);
|
||||
|
||||
for (int target = 0; target < 2; ++target) {
|
||||
ClearToSentinel();
|
||||
glUniform1i(targetLocation, target);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
const std::vector<float> first = ReadAttachment(0);
|
||||
const std::vector<float> second = ReadAttachment(1);
|
||||
EXPECT_NEAR(first[0], target == 0 ? 0.5f : 0.25f, 0.02f)
|
||||
<< "attachment 0 red with uTarget=" << target;
|
||||
EXPECT_NEAR(first[1], 0.25f, 0.02f) << "attachment 0 green with uTarget=" << target;
|
||||
EXPECT_NEAR(second[0], target == 1 ? 1.0f : 0.75f, 0.02f)
|
||||
<< "attachment 1 red with uTarget=" << target;
|
||||
EXPECT_NEAR(second[1], 0.75f, 0.02f) << "attachment 1 green with uTarget=" << target;
|
||||
}
|
||||
|
||||
glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,476 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/Glsl420DeclarationScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GLSL 4.20 DECLARATIONS THE FRONTEND USED TO REJECT OR COLLAPSE.
|
||||
//
|
||||
// GLSL 4.20 gives an array of opaque uniforms or of block instances CONSECUTIVE binding
|
||||
// points: "layout(binding = 1) uniform sampler2D goku[7]" puts goku[0] on texture unit 1
|
||||
// and goku[6] on unit 7, and the same rule holds for "layout(binding = 2) uniform GOKU
|
||||
// {...} goku[14]" over uniform buffer binding points 2..15 (GLSL 4.20 4.4.5, GL 4.6 7.6.2).
|
||||
// One qualifier, N bindings - which is exactly the part that is easy to get wrong, because
|
||||
// every element shares one declaration and one reflection record.
|
||||
//
|
||||
// Three separate mechanisms all collapsed that array down to its first element, and the
|
||||
// three cases below pin one each:
|
||||
//
|
||||
// * the SAMPLER array (Espryt): reflection names an array after its first element at
|
||||
// every location it spans, so the backend resolved "goku[0]" once per element, got one
|
||||
// backend location N times, and the per-draw pass's last glUniform1i was the only one
|
||||
// that survived. goku[0] ended up holding the LAST element's unit and goku[1..N-1] kept
|
||||
// unit 0 - so every element sampled whatever was bound to unit 0.
|
||||
// * the uniform BLOCK array (both backends): glslang reports the declared binding for
|
||||
// every expanded instance, so nothing added the element offset. glGetActiveUniformBlockiv
|
||||
// answered the base binding for all of them, and since both backends feed a block from
|
||||
// that same number at draw time, all instances also read one buffer.
|
||||
// * 'invariant' on a non-vertex stage's INPUT: legal desktop GLSL at every version, and
|
||||
// ignored where it is written, but glslang rejected it from 4.20 up - so a shader that
|
||||
// compiled as "#version 400" stopped compiling as "#version 420".
|
||||
//
|
||||
// The fourth case is the same species as the third - a legal 4.20 shader the frontend
|
||||
// refused - and lives here for that reason: atomicCounterIncrement() was rejected because
|
||||
// glslang applied its atomicAdd() extension gate to the atomicAdd() its own Vulkan-relaxed
|
||||
// lowering had just synthesized.
|
||||
//
|
||||
// Conformance cases behind these: KHR-GL42.shading_language_420pack.binding_sampler_array,
|
||||
// .binding_uniform_block_array, .qualifier_order[_block]_test_id_*, and
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kElements = 4;
|
||||
|
||||
// No vertex attributes: the quad comes from gl_VertexID, so nothing here depends on
|
||||
// the harness's attribute pinning and the fragment stage is the only thing under test.
|
||||
constexpr const char* kQuadVS = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// The red channel comes back as a BITMASK of which elements read the wrong thing, so
|
||||
// a failure names the element instead of just saying "not green". float(bad)/255.0
|
||||
// round-trips exactly through an RGBA8 target for every mask this can produce.
|
||||
constexpr const char* kSamplerArrayFS = R"(#version 420 core
|
||||
layout(binding = 1) uniform sampler2D goku[4];
|
||||
out vec4 o_color;
|
||||
void main()
|
||||
{
|
||||
const vec2 uv = vec2(0.5, 0.5);
|
||||
int bad = 0;
|
||||
if (texture(goku[0], uv) != vec4(1.0, 0.0, 0.0, 1.0)) bad |= 1;
|
||||
if (texture(goku[1], uv) != vec4(0.0, 0.0, 1.0, 1.0)) bad |= 2;
|
||||
if (texture(goku[2], uv) != vec4(1.0, 1.0, 0.0, 1.0)) bad |= 4;
|
||||
if (texture(goku[3], uv) != vec4(0.0, 1.0, 1.0, 1.0)) bad |= 8;
|
||||
o_color = vec4(float(bad) / 255.0, bad == 0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Same declaration one dimension deeper. GLSL 4.30 arrays of arrays are legal here, and
|
||||
// the elements still take consecutive units (1..4) in declaration order - but the two
|
||||
// reflections disagree about how to count them, which is the whole point of this case.
|
||||
constexpr const char* kSamplerArrayOfArraysFS = R"(#version 430 core
|
||||
layout(binding = 1) uniform sampler2D goku[2][2];
|
||||
out vec4 o_color;
|
||||
void main()
|
||||
{
|
||||
const vec2 uv = vec2(0.5, 0.5);
|
||||
int bad = 0;
|
||||
if (texture(goku[0][0], uv) != vec4(1.0, 0.0, 0.0, 1.0)) bad |= 1;
|
||||
if (texture(goku[0][1], uv) != vec4(0.0, 0.0, 1.0, 1.0)) bad |= 2;
|
||||
if (texture(goku[1][0], uv) != vec4(1.0, 1.0, 0.0, 1.0)) bad |= 4;
|
||||
if (texture(goku[1][1], uv) != vec4(0.0, 1.0, 1.0, 1.0)) bad |= 8;
|
||||
o_color = vec4(float(bad) / 255.0, bad == 0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kBlockArrayFS = R"(#version 420 core
|
||||
layout(std140, binding = 2) uniform GOKU
|
||||
{
|
||||
vec4 gohan;
|
||||
} goku[4];
|
||||
out vec4 o_color;
|
||||
void main()
|
||||
{
|
||||
int bad = 0;
|
||||
if (goku[0].gohan != vec4(1.0, 0.0, 0.0, 1.0)) bad |= 1;
|
||||
if (goku[1].gohan != vec4(0.0, 0.0, 1.0, 1.0)) bad |= 2;
|
||||
if (goku[2].gohan != vec4(1.0, 1.0, 0.0, 1.0)) bad |= 4;
|
||||
if (goku[3].gohan != vec4(0.0, 1.0, 1.0, 1.0)) bad |= 8;
|
||||
o_color = vec4(float(bad) / 255.0, bad == 0 ? 1.0 : 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The producing stage declares the varying invariant (always legal) and the consuming
|
||||
// stage redeclares it (the part that regressed at 4.20). The qualifier ORDER is the
|
||||
// shuffled one 420pack exists to allow, so this also covers the parse path the
|
||||
// qualifier_order cases exercise.
|
||||
constexpr const char* kInvariantInVS = R"(#version 420 core
|
||||
smooth invariant out highp vec4 v_data;
|
||||
void main()
|
||||
{
|
||||
v_data = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kInvariantInFS = R"(#version 420 core
|
||||
highp in smooth invariant vec4 v_data;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = v_data; }
|
||||
)";
|
||||
|
||||
// atomicCounterIncrement() is core GLSL from 4.20 and needs no extension. MobileGL
|
||||
// parses under Vulkan-relaxed rules, which rewrite it into an atomicAdd() on a buffer
|
||||
// block - and glslang then applied to its OWN rewrite the desktop-below-430 gate that
|
||||
// demands GL_ARB_shader_storage_buffer_object for atomicAdd, rejecting a shader it had
|
||||
// just accepted. The shape is lifted from
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters.
|
||||
constexpr const char* kAtomicCounterVS = R"(#version 420 core
|
||||
layout(binding = 0, offset = 0) uniform atomic_uint g_counter;
|
||||
out flat uint v_index;
|
||||
void main()
|
||||
{
|
||||
v_index = atomicCounterIncrement(g_counter);
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
default: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kAtomicCounterFS = R"(#version 420 core
|
||||
in flat uint v_index;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class Glsl420DeclarationScenario : public ScenarioTest {
|
||||
protected:
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_textures.clear();
|
||||
m_buffers.clear();
|
||||
m_programs.clear();
|
||||
m_vao = 0;
|
||||
}
|
||||
|
||||
GLuint Build(const char* vs, const char* fs) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(vs, fs, &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "program did not build: " << error;
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One 1x1 RGBA8 texture per element, each a colour whose channels are exactly 0 or
|
||||
// 255 so the shader's == comparisons are exact.
|
||||
void MakeElementTextures(const std::uint8_t colors[kElements][4]) {
|
||||
m_textures.assign(kElements, 0);
|
||||
glGenTextures(kElements, m_textures.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + 1 + i);
|
||||
glBindTexture(GL_TEXTURE_2D, m_textures[i]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, colors[i]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void MakeElementBuffers(const float values[kElements][4], GLuint firstBinding) {
|
||||
m_buffers.assign(kElements, 0);
|
||||
glGenBuffers(kElements, m_buffers.data());
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, m_buffers[i]);
|
||||
glBufferData(GL_UNIFORM_BUFFER, 4 * sizeof(float), values[i], GL_STATIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, firstBinding + i, m_buffers[i]);
|
||||
}
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
|
||||
// Draws the full-screen quad and hands back the centre pixel.
|
||||
Rgba8 DrawAndRead(GLuint program) {
|
||||
HeadlessGL& gl = Gl();
|
||||
if (m_vao == 0) glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
const Image image = ReadPixels(gl.Width(), gl.Height());
|
||||
glUseProgram(0);
|
||||
return image.At(gl.Width() / 2, gl.Height() / 2);
|
||||
}
|
||||
|
||||
// An array of ARRAYS is declined by Magma (ProgramFactory::ReflectLayout logs it and
|
||||
// VkProgramObject::declinedDescriptors then refuses every draw), which is a defined
|
||||
// outcome the case below can assert. Espryt has no such gate: it bakes the units the
|
||||
// frontend reports into its ESSL, and since the binding-qualifier seeding does not
|
||||
// walk the inner dimension every element reports unit 0 - so it samples one texture
|
||||
// four times and paints a mismatch. That gap is in the FRONTEND, one level below
|
||||
// either backend, and fixing it is the feature that would make this shape work
|
||||
// everywhere; it is not part of wiring descriptor arrays through Magma, so the
|
||||
// Espryt arm is SCOPED and the reflection half is asserted on both backends.
|
||||
bool MultiDimensionalSamplerArraysAreDeclined() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||
|
||||
// Same shape, different gap: with the compile fixed, this shader now links on
|
||||
// both backends but paints nothing on Magma - the atomic counter becomes a
|
||||
// buffer descriptor there and that half is not wired up yet (the conformance
|
||||
// case KHR-GL42.shader_image_load_store.advanced-sso-atomicCounters is where it
|
||||
// is measured). The regression this case exists for is the COMPILE, which is
|
||||
// asserted on both backends above; only the paint is scoped.
|
||||
bool AtomicCounterDrawsAreSupported() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||
|
||||
static std::string BadElements(std::uint8_t mask) {
|
||||
if (mask == 0) return "none";
|
||||
std::string out;
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
if ((mask & (1u << i)) == 0) continue;
|
||||
if (!out.empty()) out += ", ";
|
||||
out += "[" + std::to_string(i) + "]";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_buffers;
|
||||
std::vector<GLuint> m_programs;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Element k of a sampler array samples texture unit N+k - both as the API reports it and,
|
||||
// the part that was actually broken, as the draw behaves.
|
||||
TEST_F(Glsl420DeclarationScenario, SamplerArrayElementsSampleConsecutiveTextureUnits) {
|
||||
if (!Ready()) return;
|
||||
|
||||
static const std::uint8_t colors[kElements][4] = {
|
||||
{255, 0, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}, {0, 255, 255, 255}};
|
||||
MakeElementTextures(colors);
|
||||
|
||||
const GLuint program = Build(kQuadVS, kSamplerArrayFS);
|
||||
if (program == 0) return;
|
||||
|
||||
// The reported unit is the shadow the frontend seeds from the qualifier. It was
|
||||
// already right when the draw was wrong, so checking only this would have passed
|
||||
// straight through the bug - it is here to separate a reflection regression from a
|
||||
// backend one if this case ever fails again.
|
||||
glUseProgram(program);
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
const std::string name = "goku[" + std::to_string(i) + "]";
|
||||
const GLint location = glGetUniformLocation(program, name.c_str());
|
||||
ASSERT_GE(location, 0) << name << " has no location";
|
||||
GLint unit = -1;
|
||||
glGetUniformiv(program, location, &unit);
|
||||
EXPECT_EQ(unit, 1 + i) << name << " should default to texture unit " << (1 + i);
|
||||
}
|
||||
glUseProgram(0);
|
||||
|
||||
const Rgba8 centre = DrawAndRead(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre.r, 0) << "sampler array elements that read the wrong texture: " << BadElements(centre.r);
|
||||
EXPECT_EQ(centre.g, 255) << "the draw did not reach the fragment stage at all";
|
||||
}
|
||||
|
||||
// An array of ARRAYS of samplers is the shape the two reflections count differently:
|
||||
// SPIRV-Reflect reports one binding of 4 flattened descriptors, while the frontend hands out
|
||||
// uniform locations along the outer dimension only and keys the uniform by its full
|
||||
// "goku[0][0]" spelling. Magma therefore cannot address elements 1..3 of that binding, and
|
||||
// the contract this case pins is that it says so and DECLINES - the failure it must never
|
||||
// return to is resolving those elements onto whatever uniform got the next locations, which
|
||||
// is a silently wrong texture rather than a missing draw.
|
||||
//
|
||||
// Deliberately weak on the pixels for that reason: what is asserted on every backend is that
|
||||
// the program builds, the draw raises no GL error, and the process survives. Where the
|
||||
// descriptors do resolve, the colours are checked too.
|
||||
TEST_F(Glsl420DeclarationScenario, AnArrayOfSamplerArraysIsHonouredOrDeclinedCleanly) {
|
||||
if (!Ready()) return;
|
||||
|
||||
static const std::uint8_t colors[kElements][4] = {
|
||||
{255, 0, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}, {0, 255, 255, 255}};
|
||||
MakeElementTextures(colors);
|
||||
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kQuadVS, kSamplerArrayOfArraysFS, &error);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "the frontend does not build an array of sampler arrays: " << error;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
|
||||
// The reflection DOES reserve one location per flattened element, in the order
|
||||
// SPIRV-Reflect flattens them - which is the whole reason baseLocation + element is the
|
||||
// right addressing rule for a descriptor array, and would be right for this shape too.
|
||||
// What is missing is one level up: the `layout(binding = 1)` unit seeding walks the outer
|
||||
// dimension only, so all four elements report unit 0 instead of 1..4. That is why this
|
||||
// shape is declined rather than supported, and it is asserted here because the day the
|
||||
// seeding learns arrays of arrays, the decline should be revisited rather than kept.
|
||||
glUseProgram(program);
|
||||
for (int outer = 0; outer < 2; ++outer) {
|
||||
for (int inner = 0; inner < 2; ++inner) {
|
||||
const std::string name = "goku[" + std::to_string(outer) + "][" + std::to_string(inner) + "]";
|
||||
EXPECT_EQ(glGetUniformLocation(program, name.c_str()), outer * 2 + inner)
|
||||
<< name << " should hold the flattened element's own location";
|
||||
}
|
||||
}
|
||||
glUseProgram(0);
|
||||
|
||||
const Rgba8 centre = DrawAndRead(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "declining a descriptor array must not raise a GL error";
|
||||
|
||||
if (!MultiDimensionalSamplerArraysAreDeclined()) {
|
||||
GTEST_SKIP() << "the frontend's binding-qualifier seeding does not walk an array of arrays, so "
|
||||
<< Gl().BackendName() << " samples unit 0 for every element; the locations "
|
||||
<< "asserted above are the half of this case it can answer";
|
||||
}
|
||||
|
||||
// Three outcomes are possible and only two are acceptable. Green means every element
|
||||
// sampled its own unit. Black - the untouched clear - means the program was declined and
|
||||
// painted nothing, which is the documented Magma outcome. A non-zero red channel is the
|
||||
// third: the draw DID reach the fragment stage and elements read the wrong textures,
|
||||
// which is exactly the silent mismatch this decline exists to prevent.
|
||||
if (centre.g == 255) {
|
||||
EXPECT_EQ(centre.r, 0) << "elements of the array of arrays that read the wrong texture: "
|
||||
<< BadElements(centre.r);
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(centre.r, 0) << "the array of arrays was not resolved, but the draw still painted "
|
||||
"a mismatch instead of being declined: " << BadElements(centre.r);
|
||||
}
|
||||
|
||||
// Instance k of a uniform block array sits on buffer binding point N+k - again both as
|
||||
// reported and as fed to the shader.
|
||||
TEST_F(Glsl420DeclarationScenario, UniformBlockArrayInstancesTakeConsecutiveBindings) {
|
||||
if (!Ready()) return;
|
||||
|
||||
static const float values[kElements][4] = {
|
||||
{1.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f}, {1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 1.0f, 1.0f}};
|
||||
constexpr GLuint kFirstBinding = 2;
|
||||
MakeElementBuffers(values, kFirstBinding);
|
||||
|
||||
const GLuint program = Build(kQuadVS, kBlockArrayFS);
|
||||
if (program == 0) return;
|
||||
|
||||
for (int i = 0; i < kElements; ++i) {
|
||||
const std::string name = "GOKU[" + std::to_string(i) + "]";
|
||||
const GLuint index = glGetUniformBlockIndex(program, name.c_str());
|
||||
ASSERT_NE(index, static_cast<GLuint>(GL_INVALID_INDEX)) << name << " is not an active block";
|
||||
GLint binding = -1;
|
||||
glGetActiveUniformBlockiv(program, index, GL_UNIFORM_BLOCK_BINDING, &binding);
|
||||
EXPECT_EQ(binding, static_cast<GLint>(kFirstBinding) + i)
|
||||
<< name << " should start on binding point " << (kFirstBinding + i);
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the block queries left a GL error behind";
|
||||
|
||||
const Rgba8 centre = DrawAndRead(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre.r, 0) << "block array instances that read the wrong buffer: " << BadElements(centre.r);
|
||||
EXPECT_EQ(centre.g, 255) << "the draw did not reach the fragment stage at all";
|
||||
}
|
||||
|
||||
// 'invariant' written on a fragment input at #version 420. The same source compiles at
|
||||
// #version 400 on any implementation, so a version-dependent rejection is the defect.
|
||||
TEST_F(Glsl420DeclarationScenario, InvariantIsAcceptedOnANonVertexStageInput) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = Build(kInvariantInVS, kInvariantInFS);
|
||||
if (program == 0) return;
|
||||
|
||||
const Rgba8 centre = DrawAndRead(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre.g, 255) << "the invariant-qualified varying did not arrive";
|
||||
EXPECT_EQ(centre.r, 0);
|
||||
}
|
||||
|
||||
// A #version 420 shader may call atomicCounterIncrement() with no extension at all. The
|
||||
// assertion is deliberately the COMPILE, because the defect was a compile-time gate on
|
||||
// glslang's own atomic-counter lowering; the draw that follows only checks the shader
|
||||
// survives the rest of the pipeline without leaving an error behind.
|
||||
TEST_F(Glsl420DeclarationScenario, AnAtomicCounterCompilesWithoutTheSsboExtension) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(shader, 1, &kAtomicCounterVS, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
glDeleteShader(shader);
|
||||
FAIL() << "atomicCounterIncrement() at #version 420 core did not compile: " << log;
|
||||
}
|
||||
glDeleteShader(shader);
|
||||
|
||||
const GLuint program = Build(kAtomicCounterVS, kAtomicCounterFS);
|
||||
if (program == 0) return;
|
||||
|
||||
GLuint counter = 0;
|
||||
glGenBuffers(1, &counter);
|
||||
m_buffers.push_back(counter);
|
||||
const GLuint zero = 0;
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, counter);
|
||||
glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, counter);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||
|
||||
if (!AtomicCounterDrawsAreSupported()) {
|
||||
GTEST_SKIP() << "atomic-counter draws do not paint on " << Gl().BackendName()
|
||||
<< " yet; the compile above is what this case pins";
|
||||
}
|
||||
|
||||
const Rgba8 centre = DrawAndRead(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,314 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
|
||||
//
|
||||
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
|
||||
//
|
||||
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
|
||||
// images have to define layout format", which fails the whole program. That is what took the
|
||||
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
|
||||
//
|
||||
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
|
||||
// internal format to belong to one format class - so the format is not knowable when the shader
|
||||
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
|
||||
// generates and keys that program on the (unit, format) pairs it baked
|
||||
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
|
||||
//
|
||||
// Three separate things follow from "the program is built against live binding state", and each
|
||||
// one is a case below:
|
||||
//
|
||||
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
|
||||
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
|
||||
// compiled against the old format (RebindToADifferentFormatRebuilds);
|
||||
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
|
||||
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
|
||||
//
|
||||
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
|
||||
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
|
||||
// at descriptor time - so every case here runs on both backends and must agree, which is what
|
||||
// makes the ES-only machinery falsifiable rather than merely exercised.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 4;
|
||||
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
|
||||
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
|
||||
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
|
||||
constexpr GLint kImageUnit = 1;
|
||||
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
|
||||
const char* kStoreSource = R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)";
|
||||
|
||||
class ImageFormatQualifierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
GLuint MakeComputeProgram(const std::string& source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture(GLenum internalFormat) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
// Seeded to a value no dispatch writes, so "the store never happened" and "the
|
||||
// store wrote the right thing" cannot be confused.
|
||||
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
|
||||
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
zeros.data());
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
|
||||
// than through a second image uniform: a defect in the format bake would be shared by
|
||||
// a reader declared the same way and could cancel itself out.
|
||||
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
|
||||
const bool rgba = internalFormat == GL_RGBA32UI;
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
|
||||
0xFFFFFFFFu);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
|
||||
}
|
||||
|
||||
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
|
||||
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
|
||||
internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0) << "the image uniform was not reflected";
|
||||
glUniform1i(location, kImageUnit);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// The defect itself. Without the bake the ES driver refuses the program outright and the
|
||||
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
|
||||
// like, and why the seed is a value no store writes.
|
||||
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << " of a format-less writeonly image did not take the store";
|
||||
}
|
||||
}
|
||||
|
||||
// The rebuild key. The SAME program is dispatched twice with a different format bound to
|
||||
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
|
||||
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
|
||||
//
|
||||
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
|
||||
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
|
||||
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
|
||||
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
|
||||
// program was built with no format at all and never compiled. Both are kept: this one is
|
||||
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
|
||||
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint first = MakeTexture(GL_R32UI);
|
||||
const GLuint second = MakeTexture(GL_RGBA32UI);
|
||||
if (program == 0 || first == 0 || second == 0) return;
|
||||
|
||||
DispatchStore(program, first, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "the first format must work before the rebind can be blamed for anything";
|
||||
}
|
||||
|
||||
DispatchStore(program, second, GL_RGBA32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
|
||||
}
|
||||
|
||||
// ...and back, so the rebuild is not a one-way door: returning to a format the
|
||||
// program was once built against must build for it again, not resurrect a cache row.
|
||||
const GLuint third = MakeTexture(GL_R32UI);
|
||||
if (third == 0) return;
|
||||
DispatchStore(program, third, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": going back to the first format did not rebuild";
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is bound to the unit when the program links, so whatever the first build sees
|
||||
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
|
||||
// build - it is an entry point, and building there is the constraint
|
||||
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
|
||||
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
|
||||
// build attempted from the entry point before the state settles.
|
||||
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
if (program == 0) return;
|
||||
|
||||
// Use it once with NOTHING bound to the unit, which is what makes the backend build
|
||||
// against an empty binding. The dispatch writes nowhere and must not error.
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0);
|
||||
glUniform1i(location, kImageUnit);
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
|
||||
glUseProgram(0);
|
||||
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (texture == 0) return;
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the first bind after the link did not reach the shader";
|
||||
}
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative and the bake must never touch it - including when
|
||||
// the texture behind the unit has a different (but class-compatible) internal format,
|
||||
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
|
||||
// that would go wrong while every other one stayed green.
|
||||
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r32ui) writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)");
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": a declared format stopped working";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user