mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
182
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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
|
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||||
echo "usage: $0 <trace-case> [fixture-dir]" >&2
|
echo "usage: $0 <trace-case> [fixture-dir]" >&2
|
||||||
exit 2
|
exit 2
|
||||||
@@ -62,57 +66,6 @@ if [ "${case_name}" = "OpenRA" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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() {
|
fetch_file_from_mirror() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local url="$2"
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
env:
|
env:
|
||||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||||
CCACHE_COMPRESS: "true"
|
CCACHE_COMPRESS: "true"
|
||||||
@@ -41,12 +44,11 @@ jobs:
|
|||||||
gradle-version: 8.10.2
|
gradle-version: 8.10.2
|
||||||
|
|
||||||
- name: Restore ccache
|
- name: Restore ccache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache/restore@v5
|
||||||
with:
|
with:
|
||||||
path: .ccache
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
|
|
||||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||||
|
|
||||||
- name: Install ccache
|
- name: Install ccache
|
||||||
@@ -125,6 +127,28 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: ccache --show-stats
|
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
|
- name: Verify APK metadata and packaging
|
||||||
run: |
|
run: |
|
||||||
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
|
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
|
||||||
@@ -201,9 +225,41 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v6
|
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
|
- name: Fetch trace fixture
|
||||||
|
if: steps.fixture-verify.outputs.ok != 'true'
|
||||||
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
|
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
|
- name: Stage trace fixture
|
||||||
run: |
|
run: |
|
||||||
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
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
|
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)
|
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
|
||||||
fi
|
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
|
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||||
extra_retrace_args+=(--coherent-as-flush)
|
extra_retrace_args+=(--coherent-as-flush)
|
||||||
fi
|
fi
|
||||||
@@ -411,6 +470,24 @@ jobs:
|
|||||||
run_retrace || retrace_status=$?
|
run_retrace || retrace_status=$?
|
||||||
if [ "${retrace_status}" -eq 75 ]; then
|
if [ "${retrace_status}" -eq 75 ]; then
|
||||||
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
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 \
|
sh android-plugin/run-avd-ci.sh stop \
|
||||||
--avd-name "${AVD_NAME}" \
|
--avd-name "${AVD_NAME}" \
|
||||||
--emulator-log "${EMULATOR_LOG}" \
|
--emulator-log "${EMULATOR_LOG}" \
|
||||||
@@ -450,6 +527,13 @@ jobs:
|
|||||||
if [ -f "${EMULATOR_LOG}" ]; then
|
if [ -f "${EMULATOR_LOG}" ]; then
|
||||||
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
|
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
|
||||||
fi
|
fi
|
||||||
|
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
|
||||||
|
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
|
||||||
|
fi
|
||||||
|
# A vanished emulator looks identical whether the host OOM killer took
|
||||||
|
# qemu or the renderer faulted. These two say which.
|
||||||
|
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
|
||||||
|
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
|
||||||
|
|
||||||
- name: Stop Emulator
|
- name: Stop Emulator
|
||||||
if: always()
|
if: always()
|
||||||
@@ -531,22 +615,41 @@ jobs:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ((${#failed_cases[@]})); then
|
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[@]}"
|
printf ' %s\n' "${!failed_cases[@]}"
|
||||||
else
|
else
|
||||||
echo "All retrace jobs succeeded; no fixtures need to be retained."
|
echo "All retrace jobs succeeded; nothing needs to be retained."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
deleted=0
|
deleted=0
|
||||||
retained=0
|
retained=0
|
||||||
while IFS=$'\t' read -r artifact_id artifact_name; do
|
while IFS=$'\t' read -r artifact_id artifact_name; do
|
||||||
|
keep=0
|
||||||
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
|
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
|
||||||
case_name="${artifact_name#MobileGL-trace-fixture-}"
|
case_name="${artifact_name#MobileGL-trace-fixture-}"
|
||||||
if [[ -v "failed_cases[${case_name}]" ]]; then
|
if [[ -v "failed_cases[${case_name}]" ]]; then
|
||||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
keep=1
|
||||||
((retained += 1))
|
|
||||||
continue
|
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
echo "Deleting ${artifact_name} (${artifact_id})"
|
echo "Deleting ${artifact_name} (${artifact_id})"
|
||||||
|
|||||||
+197
-8
@@ -1,4 +1,4 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -11,6 +11,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
env:
|
env:
|
||||||
BUILD_DIR: build-linux
|
BUILD_DIR: build-linux
|
||||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||||
@@ -34,12 +37,11 @@ jobs:
|
|||||||
uses: lukka/get-cmake@v4.3.3
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
- name: Restore ccache
|
- name: Restore ccache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache/restore@v5
|
||||||
with:
|
with:
|
||||||
path: .ccache
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
|
|
||||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||||
|
|
||||||
- name: Prepare Vulkan SDK
|
- name: Prepare Vulkan SDK
|
||||||
@@ -83,6 +85,8 @@ jobs:
|
|||||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||||
-DMOBILEGL_BUILD_TEST=ON \
|
-DMOBILEGL_BUILD_TEST=ON \
|
||||||
-DMOBILEGL_BUILD_BENCHMARK=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 \
|
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||||
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
||||||
-DBENCHMARK_ENABLE_TESTING=OFF \
|
-DBENCHMARK_ENABLE_TESTING=OFF \
|
||||||
@@ -95,6 +99,28 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: ccache --show-stats
|
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
|
- name: Package Linux runtime
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ci-artifacts
|
mkdir -p ci-artifacts
|
||||||
@@ -110,6 +136,7 @@ jobs:
|
|||||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||||
|
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
||||||
"${SHARED_LIBS[@]}"
|
"${SHARED_LIBS[@]}"
|
||||||
|
|
||||||
- name: Upload Linux runtime
|
- name: Upload Linux runtime
|
||||||
@@ -159,12 +186,102 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
working-directory: build-linux
|
working-directory: build-linux
|
||||||
run: |
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||||
ctest -V -L unit --no-tests=error
|
ctest -V -L unit --no-tests=error
|
||||||
else
|
else
|
||||||
ctest --output-on-failure -L unit --no-tests=error
|
ctest --output-on-failure -L unit --no-tests=error
|
||||||
fi
|
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:
|
benchmark:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-linux
|
needs: build-linux
|
||||||
@@ -208,7 +325,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Benchmark
|
- name: Benchmark
|
||||||
working-directory: build-linux
|
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:
|
build-retrace:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -216,6 +344,10 @@ jobs:
|
|||||||
- build-linux
|
- build-linux
|
||||||
- test
|
- test
|
||||||
- benchmark
|
- benchmark
|
||||||
|
- integration
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
env:
|
env:
|
||||||
BUILD_DIR: build-retrace
|
BUILD_DIR: build-retrace
|
||||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||||
@@ -240,12 +372,11 @@ jobs:
|
|||||||
uses: lukka/get-cmake@v4.3.3
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
- name: Restore ccache
|
- name: Restore ccache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache/restore@v5
|
||||||
with:
|
with:
|
||||||
path: .ccache
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
|
|
||||||
${{ runner.os }}-test-${{ github.job }}-ccache-
|
${{ runner.os }}-test-${{ github.job }}-ccache-
|
||||||
|
|
||||||
- name: Prepare Vulkan SDK
|
- name: Prepare Vulkan SDK
|
||||||
@@ -311,6 +442,21 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: ccache --show-stats
|
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
|
- name: Normalize CTest command paths
|
||||||
run: |
|
run: |
|
||||||
python - <<'PY'
|
python - <<'PY'
|
||||||
@@ -343,6 +489,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test
|
- test
|
||||||
- benchmark
|
- benchmark
|
||||||
|
- integration
|
||||||
outputs:
|
outputs:
|
||||||
names: ${{ steps.trace-cases.outputs.names }}
|
names: ${{ steps.trace-cases.outputs.names }}
|
||||||
steps:
|
steps:
|
||||||
@@ -366,9 +513,41 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v6
|
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
|
- name: Fetch trace fixture
|
||||||
|
if: steps.fixture-verify.outputs.ok != 'true'
|
||||||
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
|
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
|
- name: Stage trace fixture
|
||||||
run: |
|
run: |
|
||||||
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||||
@@ -456,6 +635,8 @@ jobs:
|
|||||||
- name: Retrace and validate
|
- name: Retrace and validate
|
||||||
working-directory: build-retrace/tools/trace_replay
|
working-directory: build-retrace/tools/trace_replay
|
||||||
run: |
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||||
fi
|
fi
|
||||||
@@ -470,6 +651,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
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
|
- name: Upload actual image
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|||||||
+3
-6
@@ -7,9 +7,6 @@
|
|||||||
[submodule "3rdparty/SPIRV-Cross"]
|
[submodule "3rdparty/SPIRV-Cross"]
|
||||||
path = 3rdparty/SPIRV-Cross
|
path = 3rdparty/SPIRV-Cross
|
||||||
url = https://github.com/KhronosGroup/SPIRV-Cross.git
|
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"]
|
[submodule "3rdparty/tracy"]
|
||||||
path = 3rdparty/tracy
|
path = 3rdparty/tracy
|
||||||
url = https://github.com/wolfpld/tracy.git
|
url = https://github.com/wolfpld/tracy.git
|
||||||
@@ -34,6 +31,6 @@
|
|||||||
[submodule "3rdparty/asio"]
|
[submodule "3rdparty/asio"]
|
||||||
path = 3rdparty/asio
|
path = 3rdparty/asio
|
||||||
url = https://github.com/chriskohlhoff/asio.git
|
url = https://github.com/chriskohlhoff/asio.git
|
||||||
[submodule "3rdparty/libfork"]
|
[submodule "include/ska"]
|
||||||
path = 3rdparty/libfork
|
path = include/ska
|
||||||
url = https://github.com/ConorWilliams/libfork.git
|
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 900b29d449...6f12598784
Vendored
-1
Submodule 3rdparty/libfork deleted from 9b2b844a5f
+85
-7
@@ -20,6 +20,81 @@ set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to
|
|||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
|
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
|
||||||
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" 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()
|
endif()
|
||||||
|
|
||||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||||
@@ -201,13 +276,22 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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/LowerDrawParametersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.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/RebaseInstanceIndexPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
@@ -296,6 +380,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.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/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||||
@@ -373,13 +458,6 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
# pimpl so no consumer target needs this path.
|
# pimpl so no consumer target needs this path.
|
||||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
||||||
# The second shader-compile execution engine (MOBILEGL_ASYNC_POOL=libfork), on the
|
|
||||||
# same terms as Asio above: header-only, no add_subdirectory (its CMakeLists only
|
|
||||||
# declares an INTERFACE target plus install/test scaffolding we do not want), no link
|
|
||||||
# target, and reachable from exactly one translation unit. libfork's own
|
|
||||||
# target_compile_features asks for cxx_std_23, which this project already sets
|
|
||||||
# globally, so its C++20 coroutines need no per-source standard override.
|
|
||||||
${CMAKE_SOURCE_DIR}/3rdparty/libfork/include
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
|
|||||||
+40
-6
@@ -66,12 +66,11 @@ namespace MobileGL::MG_Config {
|
|||||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||||
// (see MG_Util/Debug/Log.cpp).
|
// (see MG_Util/Debug/Log.cpp).
|
||||||
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
|
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||||
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
|
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||||
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
|
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||||
// matrix against both execution engines. Mirroring it here would resolve to the
|
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||||
// default in exactly the tests that exist to tell the engines apart (see
|
// SpirvValidationEnabled).
|
||||||
// MG_Util/Async/ShaderCompilePool.cpp, DetectAsyncPoolEngine).
|
|
||||||
struct FeaturesTable {
|
struct FeaturesTable {
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||||
Bool DisableTimerQuery = false;
|
Bool DisableTimerQuery = false;
|
||||||
@@ -83,6 +82,14 @@ namespace MobileGL::MG_Config {
|
|||||||
#endif
|
#endif
|
||||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||||
Bool DisableSubgroup = false;
|
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.
|
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||||
Bool MagmaR11G11B10FFallback = false;
|
Bool MagmaR11G11B10FFallback = false;
|
||||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||||
@@ -90,6 +97,13 @@ namespace MobileGL::MG_Config {
|
|||||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||||
Bool AvoidSamplerMipmapMinFilter = false;
|
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
|
// 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
|
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||||
@@ -103,6 +117,13 @@ namespace MobileGL::MG_Config {
|
|||||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||||
// (negative control / driver-bug escape hatch).
|
// (negative control / driver-bug escape hatch).
|
||||||
Bool DisableUboRing = false;
|
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,
|
// 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
|
// 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
|
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||||
@@ -142,6 +163,19 @@ namespace MobileGL::MG_Config {
|
|||||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
// 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.
|
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||||
Uint32 AsyncShaderCompileThreads = 0;
|
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;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -167,13 +167,17 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||||
#endif
|
#endif
|
||||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||||
|
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||||
features.AvoidSamplerMipmapMinFilter =
|
features.AvoidSamplerMipmapMinFilter =
|
||||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
|
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
|
||||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||||
|
features.EsprytForceDepthStencilReadbackEmulation =
|
||||||
|
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
@@ -183,6 +187,8 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
|
features.AsyncOptimisticShaderStatus =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
+37
-4
@@ -9,10 +9,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// ============== Platform-specific definitions and macros ============== //
|
// ============== Platform-specific definitions and macros ============== //
|
||||||
#ifdef __ANDROID__
|
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
|
||||||
#undef __ANDROID_API__
|
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
|
||||||
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
|
// by the configure-time guard in CMakeLists.txt), not by a macro.
|
||||||
#endif
|
//
|
||||||
|
// 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
|
#ifdef _WIN32
|
||||||
#ifndef NOMINMAX
|
#ifndef NOMINMAX
|
||||||
@@ -37,6 +47,23 @@
|
|||||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||||
|
|
||||||
// ====================== MobileGL configurations ======================= //
|
// ====================== 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
|
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||||
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
||||||
#endif
|
#endif
|
||||||
@@ -68,6 +95,12 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// =============================== Utils ================================ //
|
// =============================== 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
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
#define MOBILEGL_ASSERT(condition, ...) \
|
#define MOBILEGL_ASSERT(condition, ...) \
|
||||||
do { \
|
do { \
|
||||||
|
|||||||
+2
-2
@@ -49,8 +49,8 @@
|
|||||||
#include <stacktrace>
|
#include <stacktrace>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Include FastSTL
|
// Include ska::flat_hash_map
|
||||||
#include <FastSTL/UnorderedMap.h>
|
#include <ska/flat_hash_map.hpp>
|
||||||
|
|
||||||
// Include xxHash
|
// Include xxHash
|
||||||
#include <xxhash.h>
|
#include <xxhash.h>
|
||||||
|
|||||||
@@ -940,6 +940,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// picks a whole different shader for draw_buffers without
|
// picks a whole different shader for draw_buffers without
|
||||||
// explicit_attrib_location. DirectVulkan advertises both.
|
// explicit_attrib_location. DirectVulkan advertises both.
|
||||||
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
|
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
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
@@ -960,6 +971,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
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
|
// Only advertised when the device driver actually has usable timer queries
|
||||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||||
@@ -1002,6 +1022,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||||
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||||
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||||
|
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
||||||
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
||||||
funcsTable.GL.DrawRangeElements = DrawRangeElements;
|
funcsTable.GL.DrawRangeElements = DrawRangeElements;
|
||||||
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
||||||
@@ -1151,6 +1172,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
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.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
|
||||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
||||||
|
|||||||
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,
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride);
|
GLsizei maxdrawcount, GLsizei stride);
|
||||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, 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,
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
const void* indices, GLint basevertex);
|
const void* indices, GLint basevertex);
|
||||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
|
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
|
// capability read needs no current ES context, and it stays false until
|
||||||
// the ES capabilities have been filled in.
|
// the ES capabilities have been filled in.
|
||||||
Bool AreTimerQueriesSupported();
|
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
|
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
|
||||||
// creators return null (the frontend then falls back to an immediately
|
// creators return null (the frontend then falls back to an immediately
|
||||||
// available zero result) when the calling thread does not own the ES
|
// 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();
|
Bool InProcessTeardown();
|
||||||
void EnsureProcessTeardownSentinel();
|
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.
|
// 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
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||||
// draws that provably cannot read them.
|
// 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
|
// 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.
|
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||||
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
|
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||||
// no-ops when the program does not read the corresponding builtin.
|
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||||
void SetCurrentDrawID(Uint32 drawId);
|
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
|
// 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
|
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||||
// batch and would therefore be wrong.
|
// batch and would therefore be wrong.
|
||||||
Bool CurrentProgramReadsDrawID();
|
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>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
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
|
// 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.
|
// 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) {
|
BackendPtr* Find(StateObject* stateObj) {
|
||||||
const auto entryIt = m_entries.find(stateObj);
|
const auto entryIt = m_entries.find(stateObj);
|
||||||
if (entryIt == m_entries.end()) {
|
if (entryIt == m_entries.end()) {
|
||||||
@@ -261,6 +286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// context loss.
|
// context loss.
|
||||||
Bool persistentMapped = false;
|
Bool persistentMapped = false;
|
||||||
void* persistentPtr = nullptr;
|
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
|
// Registered as the frontend's BufferBackendOps at backend init and on
|
||||||
@@ -441,6 +474,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 m_syncedConfigVersion = 0;
|
Uint32 m_syncedConfigVersion = 0;
|
||||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||||
m_syncedAttributeVersions;
|
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>
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||||
@@ -454,6 +493,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void InvalidateVAOBindingCache();
|
void InvalidateVAOBindingCache();
|
||||||
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||||
void NoteVAOIdDeleted(Uint id);
|
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 VertexArrayImpl
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
@@ -613,6 +669,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Bool m_imageBindableStorageRequired = false;
|
Bool m_imageBindableStorageRequired = false;
|
||||||
Bool m_backendStorageImmutable = 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;
|
StateTextureBasicInfo m_prevTextureInfo;
|
||||||
// Frontend content version at the last completed mipmap sync. The per-draw
|
// Frontend content version at the last completed mipmap sync. The per-draw
|
||||||
// clean probe compares this before rebuilding shape info and scanning
|
// clean probe compares this before rebuilding shape info and scanning
|
||||||
@@ -638,8 +699,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
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_syncedSamplerVersion = 0;
|
||||||
Uint16 m_syncedTextureParamsVersion = 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ActivateTextureUnit(Uint unit);
|
void ActivateTextureUnit(Uint unit);
|
||||||
@@ -657,15 +728,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
extern Uint g_activeTextureUnit;
|
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 TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
class BackendFramebufferObject {
|
class BackendFramebufferObject {
|
||||||
public:
|
public:
|
||||||
BackendFramebufferObject();
|
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,
|
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||||
FramebufferTarget asTarget);
|
FramebufferTarget asTarget);
|
||||||
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||||
@@ -680,6 +756,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendFBOId = 0;
|
Uint m_backendFBOId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
|
|
||||||
/* this will save buffers in its original form,
|
/* this will save buffers in its original form,
|
||||||
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
||||||
@@ -821,6 +898,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||||
void InvalidateFramebufferBindingCache();
|
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
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||||
@@ -997,9 +1078,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetBaseInstance(Uint32 baseInstance) const;
|
void SetBaseInstance(Uint32 baseInstance) const;
|
||||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||||
void SetDrawID(Uint32 drawId) const;
|
void SetDrawID(Uint32 drawId) const;
|
||||||
|
void SetBaseVertex(Int32 baseVertex) const;
|
||||||
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
||||||
// actually reaches a shader read rather than being discarded.
|
// actually reaches a shader read rather than being discarded.
|
||||||
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
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; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||||
// False when the last SyncToBackend could not produce a usable program (a
|
// False when the last SyncToBackend could not produce a usable program (a
|
||||||
@@ -1010,6 +1095,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
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; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||||
@@ -1025,14 +1115,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Frontend link version this backend program (and its resource caches) was
|
// Frontend link version this backend program (and its resource caches) was
|
||||||
// built from; a mismatch means every link-derived cache here is stale.
|
// built from; a mismatch means every link-derived cache here is stale.
|
||||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
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:
|
private:
|
||||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
Uint m_backendProgramId = 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;
|
Uint m_backendGlobalUBOId = 0;
|
||||||
Int m_baseInstanceUniformLocation = -1;
|
Int m_baseInstanceUniformLocation = -1;
|
||||||
Int m_drawIdUniformLocation = -1;
|
Int m_drawIdUniformLocation = -1;
|
||||||
|
Int m_baseVertexUniformLocation = -1;
|
||||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||||
Int m_indirectParamsBinding = -1;
|
Int m_indirectParamsBinding = -1;
|
||||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||||
@@ -1040,6 +1164,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Draw buffers a legacy gl_FragColor write has to reach (see
|
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||||
Uint m_fragColorBroadcastCount = 1;
|
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_isInitialized = false;
|
||||||
Bool m_backendProgramUsable = false;
|
Bool m_backendProgramUsable = false;
|
||||||
|
|
||||||
@@ -1050,6 +1176,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||||
Uint32 m_syncedLinkVersion = ~0u;
|
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;
|
SamplerPassMemo m_samplerPassMemo;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1073,26 +1206,79 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// on the backend program (eliminated as unused, or the driver lacks the entry
|
// 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
|
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||||
// record either way.
|
// 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);
|
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||||
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||||
// program that was just built. The frontend record is authoritative (only the
|
// program that was just built - best effort, on the same "only where the driver has
|
||||||
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||||
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||||
// reseed-on-rebuild in BuildProgramResourceCache.
|
|
||||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
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 PrgramImpl
|
||||||
|
|
||||||
namespace SamplerImpl {
|
namespace SamplerImpl {
|
||||||
class BackendSamplerObject {
|
class BackendSamplerObject {
|
||||||
public:
|
public:
|
||||||
BackendSamplerObject();
|
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 SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
||||||
void Bind(Uint unit);
|
void Bind(Uint unit);
|
||||||
Uint GetBackendSamplerId() const;
|
Uint GetBackendSamplerId() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendSamplerId = 0;
|
Uint m_backendSamplerId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
SamplerParameters m_cacheSamplerParameters;
|
SamplerParameters m_cacheSamplerParameters;
|
||||||
Uint16 m_syncedSamplerVersion = 0;
|
Uint16 m_syncedSamplerVersion = 0;
|
||||||
@@ -1110,12 +1296,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendRenderbufferObject {
|
class BackendRenderbufferObject {
|
||||||
public:
|
public:
|
||||||
BackendRenderbufferObject();
|
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);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
||||||
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendRBOId = 0;
|
Uint m_backendRBOId = 0;
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
||||||
Int m_cacheWidth = 0;
|
Int m_cacheWidth = 0;
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
g_resolvedTier =
|
g_resolvedTier =
|
||||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||||
&g_tierResolution);
|
&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.
|
// 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);
|
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||||
if (g_announcedTiers & bit) return;
|
if (g_announcedTiers & bit) return;
|
||||||
g_announcedTiers |= bit;
|
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 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
|
// 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
|
// its remaining feasibility checks inside its implementation, where the data it
|
||||||
// has to walk is already in hand.
|
// has to walk is already in hand.
|
||||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||||
|
Bool hasIndexBuffer) {
|
||||||
ResolveTierOnce();
|
ResolveTierOnce();
|
||||||
GLESMultiDrawMode tier = g_resolvedTier;
|
GLESMultiDrawMode tier = g_resolvedTier;
|
||||||
|
|
||||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
// 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
|
// 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
|
// 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 ||
|
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||||
tier == GLESMultiDrawMode::Compute;
|
tier == GLESMultiDrawMode::Compute;
|
||||||
if (batched && programReadsDrawID) {
|
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
|
||||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||||
: GLESMultiDrawMode::DrawElements;
|
: 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,
|
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;
|
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||||
const SizeT indexSize = IndexTypeSize(type);
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
if (indexSize == 0) return false;
|
if (indexSize == 0) return false;
|
||||||
@@ -413,10 +419,12 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
} else {
|
} else {
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(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);
|
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
}
|
}
|
||||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||||
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
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,
|
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;
|
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
if (count[i] <= 0) continue;
|
if (count[i] <= 0) continue;
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
basevertex ? basevertex[i] : 0);
|
basevertex ? basevertex[i] : 0);
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||||
return true;
|
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,
|
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);
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
if (indexSize == 0) return false;
|
if (indexSize == 0) return false;
|
||||||
|
|
||||||
@@ -479,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||||
subDrawCount, indexSize);
|
subDrawCount, indexSize);
|
||||||
if (!source) {
|
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",
|
"buffer; skipping the batch",
|
||||||
i);
|
i);
|
||||||
return false;
|
return false;
|
||||||
@@ -500,11 +511,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
if (count[i] <= 0) continue;
|
if (count[i] <= 0) continue;
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
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,
|
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||||
cursor += static_cast<SizeT>(count[i]);
|
cursor += static_cast<SizeT>(count[i]);
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||||
return true;
|
return true;
|
||||||
@@ -580,7 +596,7 @@ void main() {
|
|||||||
|
|
||||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||||
if (shader == 0) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
const char* source = kFlattenComputeSource;
|
const char* source = kFlattenComputeSource;
|
||||||
@@ -591,14 +607,14 @@ void main() {
|
|||||||
if (status != GL_TRUE) {
|
if (status != GL_TRUE) {
|
||||||
char log[1024] = {};
|
char log[1024] = {};
|
||||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
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);
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||||
if (program == 0) {
|
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);
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -609,7 +625,7 @@ void main() {
|
|||||||
if (status != GL_TRUE) {
|
if (status != GL_TRUE) {
|
||||||
char log[1024] = {};
|
char log[1024] = {};
|
||||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
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);
|
g_GLESFuncs.glDeleteProgram(program);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -619,7 +635,7 @@ void main() {
|
|||||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||||
g_computeProgramFailed = false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,8 +853,15 @@ void main() {
|
|||||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||||
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
// 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.
|
// 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;
|
FlattenedStream flattened;
|
||||||
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
|
||||||
|
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
|
||||||
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,8 +875,11 @@ void main() {
|
|||||||
return;
|
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 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;
|
Bool drawn = false;
|
||||||
switch (tier) {
|
switch (tier) {
|
||||||
@@ -861,16 +887,19 @@ void main() {
|
|||||||
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||||
break;
|
break;
|
||||||
case GLESMultiDrawMode::MultiIndirect:
|
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;
|
break;
|
||||||
case GLESMultiDrawMode::Indirect:
|
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;
|
break;
|
||||||
case GLESMultiDrawMode::BaseVertex:
|
case GLESMultiDrawMode::BaseVertex:
|
||||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||||
break;
|
break;
|
||||||
case GLESMultiDrawMode::DrawElements:
|
case GLESMultiDrawMode::DrawElements:
|
||||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||||
|
feedBaseVertex);
|
||||||
break;
|
break;
|
||||||
case GLESMultiDrawMode::Compute:
|
case GLESMultiDrawMode::Compute:
|
||||||
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
// 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
|
// 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
|
// rewritten index stream where it does not. Both are safe for any batch these
|
||||||
// entry points can receive.
|
// 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) {
|
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",
|
"the batch was dropped",
|
||||||
drawcount, mode, type);
|
drawcount, mode, type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <MG_Util/Math/HalfFloat.h>
|
#include <MG_Util/Math/HalfFloat.h>
|
||||||
#include <MG_Util/Math/SmallFloat.h>
|
#include <MG_Util/Math/SmallFloat.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -416,10 +417,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
result = std::regex_replace(result, pattern, "$1flat $2");
|
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) {
|
switch (shaderType) {
|
||||||
case GL_VERTEX_SHADER:
|
case GL_VERTEX_SHADER:
|
||||||
addFlatQualifier("out");
|
addFlatQualifier("out");
|
||||||
break;
|
break;
|
||||||
|
case GL_TESS_CONTROL_SHADER:
|
||||||
|
case GL_TESS_EVALUATION_SHADER:
|
||||||
case GL_GEOMETRY_SHADER:
|
case GL_GEOMETRY_SHADER:
|
||||||
addFlatQualifier("in");
|
addFlatQualifier("in");
|
||||||
addFlatQualifier("out");
|
addFlatQualifier("out");
|
||||||
@@ -434,6 +452,175 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return result;
|
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) {
|
String RemoveLayoutBinding(const String& glslCode) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -470,6 +657,352 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return result;
|
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 {
|
namespace {
|
||||||
// How a lookup carries its level of detail, and how many arguments it takes
|
// How a lookup carries its level of detail, and how many arguments it takes
|
||||||
// before the optional bias.
|
// before the optional bias.
|
||||||
@@ -527,7 +1060,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
String EmulateTextureLodBias(const String& glslCode) {
|
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -588,6 +1121,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (samplerIt == samplerNames.end()) continue;
|
if (samplerIt == samplerNames.end()) continue;
|
||||||
|
|
||||||
const String& biasName = samplerIt->second;
|
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) {
|
if (form->explicitLodArg >= 0) {
|
||||||
// Explicit LOD: the bias adds to it, as Vulkan does for
|
// Explicit LOD: the bias adds to it, as Vulkan does for
|
||||||
// OpImageSampleExplicitLod and as the CTS reference expects.
|
// OpImageSampleExplicitLod and as the CTS reference expects.
|
||||||
@@ -638,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
|
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;
|
return (rowBytes + align - 1) / align * align;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
// Walks the client-side destination the PACK parameters describe and hands each row to
|
||||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
|
||||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
// texels. Shared by the converting and the raw-word stores so both address the destination -
|
||||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
// and feed the bound pixel-pack buffer - identically.
|
||||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
// 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).
|
// 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
|
// Per the GL addressing rules, slice k row j lands at
|
||||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
template <typename FillRow>
|
||||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||||
void* pixels, Bool applyPackImageParams) {
|
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
const auto& pixelPackBufferObject =
|
||||||
if (dstPixelBytes == 0) {
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PackedReadbackLayout packedLayout{};
|
|
||||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
|
||||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
|
||||||
|
|
||||||
const auto& pixelPackBufferObject =
|
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
// 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
|
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||||
// rows are written so skip regions of the destination stay untouched.
|
if (pixelPackBufferObject) {
|
||||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||||
const SizeT imageRows =
|
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||||
applyPackImageParams && packParams.ImageHeight > 0
|
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||||
? static_cast<SizeT>(packParams.ImageHeight)
|
return true;
|
||||||
: 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 srcComponentSize = GetReadbackComponentSize(wideType);
|
Vector<Uint8> convertedRow(dstRowBytes);
|
||||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
|
||||||
Vector<Uint8> convertedRow(dstRowBytes);
|
|
||||||
|
|
||||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
fillRow(slice, row, convertedRow.data());
|
||||||
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);
|
|
||||||
|
|
||||||
if (packParams.SwapBytes) {
|
if (packParams.SwapBytes && swapGroupSize > 1) {
|
||||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
|
||||||
if (groupSize > 1) {
|
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
|
||||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
|
||||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||||
static_cast<SizeT>(row) * dstRowStride;
|
static_cast<SizeT>(row) * dstRowStride;
|
||||||
if (pixelPackBufferObject) {
|
if (pixelPackBufferObject) {
|
||||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||||
pboBaseOffset + dstOffset);
|
pboBaseOffset + dstOffset);
|
||||||
} else {
|
} else {
|
||||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (pixelPackBufferObject) {
|
if (pixelPackBufferObject) {
|
||||||
// WritebackFromBackend bumps change serials with no backend op; re-open
|
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||||
// the buffer draw-clean memos (once for the whole row loop).
|
// the buffer draw-clean memos (once for the whole row loop).
|
||||||
@@ -1147,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
return true;
|
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 ReadbackImpl
|
||||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
} // 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,
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
void* pixels, Bool applyPackImageParams);
|
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 ReadbackImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
@@ -130,7 +140,68 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
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);
|
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
|
// 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.
|
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
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
|
// 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
|
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||||
// there is nothing to rewrite.
|
// 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 PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -517,6 +517,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
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_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||||
E_GL_ARB_explicit_attrib_location,
|
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
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
@@ -539,6 +548,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
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);
|
// 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
|
// only advertised when the device actually supports timestamp queries and the
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||||
@@ -768,14 +786,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||||
m_dynamicParameters.MaxVertexAttribs = std::min(
|
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||||
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
// 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.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
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.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.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||||
@@ -843,7 +895,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
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 =
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||||
|
|||||||
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
drawBuffer->SyncPersistentMappedRange();
|
drawBuffer->SyncPersistentMappedRange();
|
||||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
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 nullptr;
|
||||||
}
|
}
|
||||||
return drawBuffer->MappedData() + commandOffset;
|
return drawBuffer->MappedData() + commandOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!indirect) {
|
if (!indirect) {
|
||||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
stride = sizeof(DrawArraysIndirectCommand);
|
stride = sizeof(DrawArraysIndirectCommand);
|
||||||
}
|
}
|
||||||
if (stride < static_cast<GLsizei>(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));
|
stride, sizeof(DrawArraysIndirectCommand));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
stride = sizeof(DrawArraysIndirectCommand);
|
stride = sizeof(DrawArraysIndirectCommand);
|
||||||
}
|
}
|
||||||
if (stride < static_cast<GLsizei>(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));
|
stride, sizeof(DrawArraysIndirectCommand));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parameterBuffer->SyncPersistentMappedRange();
|
parameterBuffer->SyncPersistentMappedRange();
|
||||||
if (parameterBuffer->MappedData() == nullptr) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
if (indexSize == 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,9 +801,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// already recorded the new binding on the program - which is what reseeds this cache
|
// 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
|
// 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.
|
// (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);
|
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
|
||||||
if (blockIndex == GL_INVALID_INDEX) return;
|
if (blockIndex == GL_INVALID_INDEX) return;
|
||||||
|
auto& cache = GetProgramResourceCache(*programObject);
|
||||||
|
if (blockIndex >= cache.storageBlocks.size()) return;
|
||||||
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
||||||
}
|
}
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
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) {
|
if (drawcount <= 0) {
|
||||||
return;
|
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{};
|
MultiDrawIndexedCmd payload{};
|
||||||
payload.mode = mode;
|
payload.mode = mode;
|
||||||
payload.indexBufferView.indexType = type;
|
payload.indexBufferView.indexType = type;
|
||||||
@@ -977,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// shift - the hardware divide was the hottest instruction of this loop.
|
// shift - the hardware divide was the hottest instruction of this loop.
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
if (indexSize == 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IsValid()) {
|
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));
|
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
|
// 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.
|
// rather than trade a lost device for a barrier into a closed buffer.
|
||||||
if (frame.hasCommandBufferRecorded) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
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.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||||
@@ -252,6 +253,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline pipeline = CreatePipeline(payload);
|
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_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||||
m_frameCounter});
|
m_frameCounter});
|
||||||
return pipeline;
|
return pipeline;
|
||||||
@@ -389,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||||
|
|
||||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
vpci.viewportCount = 1;
|
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||||
vpci.scissorCount = 1;
|
// 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};
|
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||||
raster.polygonMode = payload.polygonMode;
|
raster.polygonMode = payload.polygonMode;
|
||||||
@@ -458,9 +480,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
blend.attachmentCount = payload.colorAttachmentCount;
|
blend.attachmentCount = payload.colorAttachmentCount;
|
||||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
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};
|
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||||
gpi.pStages = payload.stages->data();
|
gpi.pStages = effectiveStages->data();
|
||||||
gpi.pVertexInputState = payload.vertexInputState;
|
gpi.pVertexInputState = payload.vertexInputState;
|
||||||
gpi.pInputAssemblyState = &ia;
|
gpi.pInputAssemblyState = &ia;
|
||||||
gpi.pTessellationState =
|
gpi.pTessellationState =
|
||||||
@@ -477,6 +546,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
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) {
|
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",
|
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),
|
VkResultToString(result),
|
||||||
@@ -507,6 +583,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
||||||
payload.vertexInputState->vertexBindingDescriptionCount,
|
payload.vertexInputState->vertexBindingDescriptionCount,
|
||||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
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) {
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
const auto& attachment = payload.colorBlendAttachments[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",
|
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
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 {
|
class PipelineFactory {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -32,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool primitiveRestartEnable = false;
|
Bool primitiveRestartEnable = false;
|
||||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||||
Uint32 patchControlPoints = 3;
|
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;
|
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||||
@@ -61,7 +79,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool fragmentReplacesDepth = false;
|
Bool fragmentReplacesDepth = false;
|
||||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
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;
|
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);
|
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
|
#include "PipelineFactory.h"
|
||||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||||
@@ -32,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
CombinedImageSampler,
|
CombinedImageSampler,
|
||||||
UniformTexelBuffer,
|
UniformTexelBuffer,
|
||||||
StorageBuffer,
|
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 {
|
enum class CompileOptionBit : Uint {
|
||||||
@@ -52,6 +59,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recorded while GL transform feedback is active, so plain draws keep the
|
// recorded while GL transform feedback is active, so plain draws keep the
|
||||||
// undecorated variant.
|
// undecorated variant.
|
||||||
XfbCapture = 1 << 6,
|
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 CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -62,6 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||||
Vector<VkShaderModule> modules;
|
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)
|
// Layout data (previously in separate VkProgramLayout)
|
||||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
@@ -75,8 +98,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Uint32> activeBindings;
|
Vector<Uint32> activeBindings;
|
||||||
Vector<Uint32> dynamicBindings;
|
Vector<Uint32> dynamicBindings;
|
||||||
Vector<Int> uniformBlockIndexByBinding;
|
Vector<Int> uniformBlockIndexByBinding;
|
||||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
// Descriptor count per binding (1 except for a descriptor ARRAY - a UBO or storage
|
||||||
// binding with descriptorCount = N).
|
// block instance array, an image uniform array or a sampler uniform array - each of
|
||||||
|
// which occupies one binding with descriptorCount = N).
|
||||||
Vector<Uint16> bindingDescriptorCounts;
|
Vector<Uint16> bindingDescriptorCounts;
|
||||||
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
||||||
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
||||||
@@ -85,6 +109,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Int> samplerUniformLocationByBinding;
|
Vector<Int> samplerUniformLocationByBinding;
|
||||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
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<VkFormat> storageImageFormatByBinding;
|
||||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||||
Vector<String> storageBlockNameByBinding;
|
Vector<String> storageBlockNameByBinding;
|
||||||
@@ -92,6 +121,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Set once during ReflectLayout so the per-draw path can skip the whole
|
// Set once during ReflectLayout so the per-draw path can skip the whole
|
||||||
// storage-image preparation for the overwhelming majority of programs.
|
// storage-image preparation for the overwhelming majority of programs.
|
||||||
Bool hasStorageImages = false;
|
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;
|
Int globalUboBinding = -1;
|
||||||
Uint32 activeVertexInputLocationMask = 0;
|
Uint32 activeVertexInputLocationMask = 0;
|
||||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||||
@@ -104,6 +146,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
Bool fragmentReplacesDepth = false;
|
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
|
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||||
@@ -118,6 +187,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
hash = other.hash;
|
hash = other.hash;
|
||||||
stages = std::move(other.stages);
|
stages = std::move(other.stages);
|
||||||
modules = std::move(other.modules);
|
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;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
@@ -136,6 +213,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||||
hasStorageImages = other.hasStorageImages;
|
hasStorageImages = other.hasStorageImages;
|
||||||
|
declinedDescriptors = other.declinedDescriptors;
|
||||||
globalUboBinding = other.globalUboBinding;
|
globalUboBinding = other.globalUboBinding;
|
||||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||||
vertexInputTypes = other.vertexInputTypes;
|
vertexInputTypes = other.vertexInputTypes;
|
||||||
@@ -145,11 +223,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
other.hasStorageImages = false;
|
other.hasStorageImages = false;
|
||||||
|
other.declinedDescriptors = false;
|
||||||
other.globalUboBinding = -1;
|
other.globalUboBinding = -1;
|
||||||
other.activeVertexInputLocationMask = 0;
|
other.activeVertexInputLocationMask = 0;
|
||||||
other.activeFragmentOutputLocationMask = 0;
|
other.activeFragmentOutputLocationMask = 0;
|
||||||
@@ -157,6 +239,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
@@ -167,6 +252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
hash = other.hash;
|
hash = other.hash;
|
||||||
stages = std::move(other.stages);
|
stages = std::move(other.stages);
|
||||||
modules = std::move(other.modules);
|
modules = std::move(other.modules);
|
||||||
|
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||||
descriptorSetLayout = other.descriptorSetLayout;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
@@ -185,6 +271,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||||
hasStorageImages = other.hasStorageImages;
|
hasStorageImages = other.hasStorageImages;
|
||||||
|
declinedDescriptors = other.declinedDescriptors;
|
||||||
globalUboBinding = other.globalUboBinding;
|
globalUboBinding = other.globalUboBinding;
|
||||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||||
vertexInputTypes = other.vertexInputTypes;
|
vertexInputTypes = other.vertexInputTypes;
|
||||||
@@ -194,11 +281,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
|
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||||
|
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
other.hasStorageImages = false;
|
other.hasStorageImages = false;
|
||||||
|
other.declinedDescriptors = false;
|
||||||
other.globalUboBinding = -1;
|
other.globalUboBinding = -1;
|
||||||
other.activeVertexInputLocationMask = 0;
|
other.activeVertexInputLocationMask = 0;
|
||||||
other.activeFragmentOutputLocationMask = 0;
|
other.activeFragmentOutputLocationMask = 0;
|
||||||
@@ -206,6 +297,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.readsBaseVertexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -233,6 +327,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
modules.clear();
|
modules.clear();
|
||||||
stages.clear();
|
stages.clear();
|
||||||
|
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,13 +351,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||||
VkProgramObject::s_device = device;
|
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;
|
ProgramFactory(const ProgramFactory&) = delete;
|
||||||
|
|
||||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||||
const VkProgramObject& GetOrCreateProgram(
|
const VkProgramObject& GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
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
|
// 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.
|
// 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
|
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||||
@@ -291,6 +400,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
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:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
@@ -304,11 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
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,
|
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
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;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
Uint32 m_maxBindings = 0;
|
Uint32 m_maxBindings = 0;
|
||||||
@@ -320,12 +471,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True only when the logical device enabled both
|
// True only when the logical device enabled both
|
||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
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;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
Uint64 m_frameCounter = 0;
|
||||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||||
Uint64 m_cacheStructureEpoch = 1;
|
Uint64 m_cacheStructureEpoch = 1;
|
||||||
IEvictionObserver* m_evictionObserver = nullptr;
|
IEvictionObserver* m_evictionObserver = nullptr;
|
||||||
|
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||||
|
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||||
|
// ever built from one keeps referencing its module. A failed build is cached as
|
||||||
|
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||||
|
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
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);
|
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||||
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
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
|
// 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.
|
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||||
// One record per visited CombinedImageSampler binding (post fallback substitution,
|
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
|
||||||
// in binding order): the resolved texture and effective sampler, as never-reused
|
// in binding order, and within a binding in array-element order): the resolved texture
|
||||||
// lifetime ids so a freed-and-reallocated object at the same heap address can only
|
// and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
|
||||||
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo).
|
// 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 {
|
struct SampledBindingRecord {
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
Uint64 samplerLifetimeId = 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
|
// texture after the fallback substitution (may still be null when no fallback
|
||||||
// exists), effective sampler = unit override else the texture's own sampler.
|
// exists), effective sampler = unit override else the texture's own sampler.
|
||||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
// 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,
|
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,
|
MG_State::GLState::ITextureObject*& outTexture,
|
||||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
// 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.
|
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||||
const MG_State::GLState::ProgramObject& program,
|
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;
|
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
|
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||||
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
// (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,
|
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
VkDescriptorImageInfo& outImageInfo,
|
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||||
Bool trustUnchangedHint = false) const;
|
Bool trustUnchangedHint = false) const;
|
||||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||||
VkDescriptorImageInfo& outImageInfo) const;
|
VkDescriptorImageInfo& outImageInfo) const;
|
||||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
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,
|
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
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,
|
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
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
|
// 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.
|
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
|
||||||
struct UboBindResult {
|
struct UboBindResult {
|
||||||
@@ -341,6 +363,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// proves every resolve input unchanged; cleared with the per-frame reset
|
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||||
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||||
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
// 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{};
|
VkDescriptorImageInfo info{};
|
||||||
Bool infoValid = false;
|
Bool infoValid = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkFormat sourceVkFormat =
|
const VkFormat sourceVkFormat =
|
||||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
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",
|
"enabled but cannot be mapped to a VkFormat",
|
||||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||||
unsupportedAttribMask |= (1u << location);
|
unsupportedAttribMask |= (1u << location);
|
||||||
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||||
vkFormat = fallbackFormat;
|
vkFormat = fallbackFormat;
|
||||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
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 "
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||||
"(type=%s size=%d normalized=%s integer=%s)",
|
"(type=%s size=%d normalized=%s integer=%s)",
|
||||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||||
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (conversion == VertexStreamConversion::None) {
|
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",
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||||
location, static_cast<Int>(sourceVkFormat),
|
location, static_cast<Int>(sourceVkFormat),
|
||||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
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);
|
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||||
if (attribByteSize == 0) {
|
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",
|
"enabled but cannot be sized",
|
||||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||||
unsupportedAttribMask |= (1u << location);
|
unsupportedAttribMask |= (1u << location);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint32 sourceStride =
|
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
// "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 ||
|
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||||
attr.Type == DataType::Uint2101010Rev;
|
attr.Type == DataType::Uint2101010Rev;
|
||||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
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
|
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||||
// attribute into a tightly packed transient stream without changing its format.
|
// attribute into a tightly packed transient stream without changing its format.
|
||||||
conversion = VertexStreamConversion::Repack;
|
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",
|
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||||
location, attr.Offset, sourceStride, requiredAlignment);
|
location, attr.Offset, sourceStride, requiredAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 stride = sourceStride;
|
Uint32 stride = sourceStride;
|
||||||
if (conversion == VertexStreamConversion::Repack) {
|
// A converted stream is tightly packed, so its stride is the converted element
|
||||||
stride = static_cast<Uint32>(attribByteSize);
|
// size - unless the source stride is zero, which does not describe a packing at
|
||||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
// 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 =
|
const VkVertexInputRate inputRate =
|
||||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||||
|
|||||||
@@ -111,10 +111,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||||
// so INSERT invalidates references to stored values. The draw path (and
|
// invalidates references to stored values - and so does ERASE, which shifts
|
||||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
// the rest of the probe cluster into the hole and therefore moves entries
|
||||||
// only the unique_ptr cell moves, never the pointee.
|
// 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;
|
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
Uint64 m_frameBoundaryCounter = 0;
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
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_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_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
|
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||||
@@ -161,12 +165,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::CollectAllDeferredReleases() {
|
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) {
|
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
}
|
}
|
||||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
|
||||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::NotifyDeviceIdle() {
|
void VkBufferManager::NotifyDeviceIdle() {
|
||||||
@@ -287,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.requiredFlags = requiredFlags,
|
.requiredFlags = requiredFlags,
|
||||||
});
|
});
|
||||||
if (!created || resource.buffer.Map() == nullptr) {
|
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));
|
static_cast<unsigned long long>(size));
|
||||||
resource.buffer.Destroy();
|
resource.buffer.Destroy();
|
||||||
resource.storageSize = 0;
|
resource.storageSize = 0;
|
||||||
@@ -309,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||||
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||||
resource.pendingFullUpload = true;
|
resource.pendingFullUpload = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -368,6 +383,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
BumpSliceEpoch(*resource);
|
BumpSliceEpoch(*resource);
|
||||||
// Any cached streaming slice refers to the previous contents.
|
// Any cached streaming slice refers to the previous contents.
|
||||||
resource->transientFrameSerial = 0;
|
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()) {
|
if (!resource->buffer.IsValid()) {
|
||||||
return; // streaming-only resource: shadow + serial are enough
|
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)) {
|
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;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!IsResourceBusy(*resource)) {
|
if (!IsResourceBusy(*resource)) {
|
||||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(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;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -450,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
||||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(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;
|
resource->pendingFullUpload = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -542,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
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->buffer.Destroy();
|
||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->usageFlags = 0;
|
resource->usageFlags = 0;
|
||||||
@@ -599,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case BufferKind::Uniform:
|
case BufferKind::Uniform:
|
||||||
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
||||||
case BufferKind::TextureBuffer:
|
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:
|
case BufferKind::ShaderStorage:
|
||||||
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
|
||||||
case BufferKind::Indirect:
|
case BufferKind::Indirect:
|
||||||
|
|||||||
@@ -102,10 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Recreate all per-frame transient arenas
|
// Recreate all per-frame transient arenas
|
||||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
// Drains every frame slot's deferred buffer/resource releases. Only valid when
|
||||||
// transient arena's parked superseded blocks). Only valid when the
|
// the caller has proven every queue submission complete; used by the present-less
|
||||||
// caller has proven every queue submission complete; used by the
|
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
|
||||||
// present-less frame-boundary drain.
|
// superseded blocks: those are still named by this frame's slices (see the
|
||||||
|
// definition), and only a frame rewind retires them.
|
||||||
void CollectAllDeferredReleases();
|
void CollectAllDeferredReleases();
|
||||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||||
void NotifyDeviceIdle();
|
void NotifyDeviceIdle();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkResult result =
|
const VkResult result =
|
||||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||||
if (result != VK_SUCCESS) {
|
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_allocator = nullptr;
|
||||||
m_buffer = VK_NULL_HANDLE;
|
m_buffer = VK_NULL_HANDLE;
|
||||||
m_allocation = nullptr;
|
m_allocation = nullptr;
|
||||||
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
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;
|
m_mappedData = nullptr;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool wasMapped = IsMapped();
|
const Bool wasMapped = IsMapped();
|
||||||
void* mapped = wasMapped ? m_mappedData : Map();
|
void* mapped = wasMapped ? m_mappedData : Map();
|
||||||
if (mapped == nullptr) {
|
if (mapped == nullptr) {
|
||||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||||
if (flushResult != VK_SUCCESS) {
|
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) {
|
if (!wasMapped) {
|
||||||
Unmap();
|
Unmap();
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||||
if (result != VK_SUCCESS) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!attachment.IsComplete()) {
|
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,
|
drawBufferIndex,
|
||||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||||
fbo.GetExternalIndex());
|
fbo.GetExternalIndex());
|
||||||
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto* texture = attachment.GetTexture().get();
|
auto* texture = attachment.GetTexture().get();
|
||||||
if (texture == nullptr) {
|
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,
|
drawBufferIndex,
|
||||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||||
fbo.GetExternalIndex());
|
fbo.GetExternalIndex());
|
||||||
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
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->GetSamples(),
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||||
&imageFormatProperties);
|
&imageFormatProperties);
|
||||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
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>(format),
|
||||||
static_cast<Int>(sampleCount),
|
static_cast<Int>(sampleCount),
|
||||||
renderbuffer->GetExternalIndex());
|
renderbuffer->GetExternalIndex());
|
||||||
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
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",
|
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||||
continue;
|
continue;
|
||||||
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||||
|
|
||||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
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",
|
"using LOAD_OP_DONT_CARE",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||||
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||||
if (hasDistinctDepthAndStencilAttachments) {
|
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());
|
fbo.GetExternalIndex());
|
||||||
}
|
}
|
||||||
if (selectedDepthStencilAttachment != nullptr) {
|
if (selectedDepthStencilAttachment != nullptr) {
|
||||||
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
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",
|
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||||
depthAttachmentId);
|
depthAttachmentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::swap(layers, that.layers);
|
std::swap(layers, that.layers);
|
||||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
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(
|
RenderPassEntry(
|
||||||
Uint64 hash,
|
Uint64 hash,
|
||||||
VkRenderPass renderpass,
|
VkRenderPass renderpass,
|
||||||
@@ -315,26 +351,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 deferredAtFrame = 0;
|
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
|
// 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
|
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||||
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||||
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
|
// materializes the source's pending clear, which looks that same resource up again. Growing
|
||||||
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
|
// an open-addressed table relocates every element, so the cached pointer went on to name
|
||||||
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
|
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
|
||||||
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
|
// out at "source image layout is undefined", silently dropping the blit -
|
||||||
// relocation the cached pointer names freed storage still holding the pre-clear
|
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
|
||||||
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
|
// iterations that grew the table.
|
||||||
// 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
|
// 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,
|
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||||
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
// 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
|
// 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
|
// ordering rule. m_textureResources is node-based for the same reason.
|
||||||
// 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.
|
// 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;
|
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||||
|
|||||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||||
|
|
||||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||||
oldResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||||
|
|
||||||
Vector<VkImageCopy> copyRegions;
|
Vector<VkImageCopy> copyRegions;
|
||||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ok = VkTextureManager::TransitionImageLayout(
|
ok = VkTextureManager::TransitionImageLayout(
|
||||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||||
newResource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||||
|
|
||||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||||
@@ -950,7 +947,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
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,
|
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
|
||||||
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
|
||||||
if (perMipSampledView == VK_NULL_HANDLE) {
|
if (perMipSampledView == VK_NULL_HANDLE) {
|
||||||
@@ -974,13 +972,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return resource->sampledView;
|
return resource->sampledView;
|
||||||
}
|
}
|
||||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
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(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format));
|
static_cast<Int>(resource->format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
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),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1000,7 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
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)",
|
"for textureId=%d (available=0x%x)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
@@ -1014,7 +1012,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
if (view == VK_NULL_HANDLE) {
|
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),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1042,14 +1040,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
format = resource->format;
|
format = resource->format;
|
||||||
}
|
}
|
||||||
if (!AreStorageImageViewFormatsCompatible(resource->format, 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(),
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
static_cast<Int>(resource->format));
|
static_cast<Int>(resource->format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (format != resource->format &&
|
if (format != resource->format &&
|
||||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
(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),
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1069,7 +1067,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
break;
|
break;
|
||||||
case VK_IMAGE_VIEW_TYPE_3D:
|
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());
|
__func__, texture.GetExternalIndex());
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
default:
|
default:
|
||||||
@@ -1078,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
if (viewType != resource->viewType) {
|
if (viewType != resource->viewType) {
|
||||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
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);
|
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -1113,7 +1111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
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)",
|
"(available=0x%x)",
|
||||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
@@ -1124,7 +1122,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||||
if (view == VK_NULL_HANDLE) {
|
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),
|
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||||
static_cast<Int>(format));
|
static_cast<Int>(format));
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
@@ -1190,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool lowerTransitioned = TransitionImageLayout(
|
const Bool lowerTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
resource.aspect, 0, writtenMipLevel);
|
||||||
MOBILEGL_ASSERT(lowerTransitioned,
|
MOBILEGL_ASSERT(lowerTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1202,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool upperTransitioned = TransitionImageLayout(
|
const Bool upperTransitioned = TransitionImageLayout(
|
||||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||||
resource.arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(upperTransitioned,
|
MOBILEGL_ASSERT(upperTransitioned,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||||
texture->GetExternalIndex());
|
texture->GetExternalIndex());
|
||||||
@@ -1222,7 +1219,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
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());
|
texture.GetExternalIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1256,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||||
s_sampledReadStages, srcAccessMask,
|
s_sampledReadStages, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||||
resource->arrayLayers);
|
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
StampResourceRecordingUse(*resource);
|
StampResourceRecordingUse(*resource);
|
||||||
@@ -1287,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
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",
|
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
@@ -1354,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||||
Uint32 layerCount) {
|
Uint32 levelCount) {
|
||||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||||
@@ -1380,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||||
barrier.subresourceRange.levelCount = levelCount;
|
barrier.subresourceRange.levelCount = levelCount;
|
||||||
barrier.subresourceRange.baseArrayLayer = 0;
|
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);
|
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||||
|
|
||||||
trackedLayout = newLayout;
|
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).
|
// 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
|
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||||
// assertion would take the whole process down instead.
|
// 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",
|
"mipLevels=%u vkViewType=%d",
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
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
|
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||||
// this texture; failing creation would lose the texture entirely, so retry
|
// this texture; failing creation would lose the texture entirely, so retry
|
||||||
// as a plain immutable-format image.
|
// 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 "
|
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||||
"will be unavailable for it)",
|
"will be unavailable for it)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
__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
|
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
// 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 "
|
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||||
"unavailable for it)",
|
"unavailable for it)",
|
||||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||||
@@ -1852,7 +1854,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkResult createImageResult =
|
const VkResult createImageResult =
|
||||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||||
if (createImageResult != VK_SUCCESS) {
|
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",
|
"mips=%u samples=%d format=%d",
|
||||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||||
@@ -2238,7 +2242,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource.fullView == VK_NULL_HANDLE) {
|
if (resource.fullView == VK_NULL_HANDLE) {
|
||||||
return false;
|
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,
|
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
|
||||||
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
|
||||||
if (resource.sampledView == VK_NULL_HANDLE) {
|
if (resource.sampledView == VK_NULL_HANDLE) {
|
||||||
@@ -2424,7 +2429,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
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());
|
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||||
for (const auto& item : uploadItems) {
|
for (const auto& item : uploadItems) {
|
||||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||||
@@ -2601,7 +2606,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
uploadSrcAccessMask,
|
uploadSrcAccessMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
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");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||||
|
|
||||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||||
@@ -2705,7 +2710,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
s_sampledReadStages,
|
s_sampledReadStages,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
VK_ACCESS_SHADER_READ_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");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||||
outResource.layout = finalLayout;
|
outResource.layout = finalLayout;
|
||||||
|
|
||||||
@@ -2860,10 +2865,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) {
|
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
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) {
|
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
|
||||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
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.
|
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
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 VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
static Bool AreStorageImageViewFormatsCompatible(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,
|
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||||
Uint32 layerCount = 1);
|
|
||||||
|
|
||||||
SizeT CollectGarbage();
|
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.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||||
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||||
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
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);
|
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& poolState : m_pools) {
|
for (auto& poolState : m_pools) {
|
||||||
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||||
Shutdown();
|
Shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& poolState = m_pools[frameIndex];
|
auto& poolState = m_pools[frameIndex];
|
||||||
if (poolState.cursor >= m_slotsPerPool) {
|
if (poolState.cursor >= m_slotsPerPool) {
|
||||||
if (!poolState.exhaustionWarned) {
|
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",
|
"this frame fall back to the frontend path",
|
||||||
frameIndex, m_slotsPerPool);
|
frameIndex, m_slotsPerPool);
|
||||||
poolState.exhaustionWarned = true;
|
poolState.exhaustionWarned = true;
|
||||||
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
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);
|
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||||
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (resultWithAvailability[1] == 0) {
|
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);
|
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||||
// expects command recording to be active and any render pass already ended.
|
// 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,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
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
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
@@ -363,6 +368,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 samplerBinding = 0;
|
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 {
|
struct DeferredDepthMipmapCleanup {
|
||||||
Vector<VkImageView> imageViews;
|
Vector<VkImageView> imageViews;
|
||||||
Vector<VkFramebuffer> framebuffers;
|
Vector<VkFramebuffer> framebuffers;
|
||||||
@@ -445,15 +475,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void* m_platformDisplay = nullptr;
|
void* m_platformDisplay = nullptr;
|
||||||
void* m_platformLibrary = nullptr;
|
void* m_platformLibrary = nullptr;
|
||||||
void* m_platformCloseDisplay = nullptr;
|
void* m_platformCloseDisplay = nullptr;
|
||||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
||||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
// CreateInstance() from the enumerated instance extensions. On desktop an
|
||||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
||||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
// 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;
|
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
|
// 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
|
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||||
@@ -519,6 +547,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||||
Bool m_primitiveTopologyListRestartFeatureEnabled = 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
|
// 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
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||||
@@ -745,6 +779,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
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
|
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||||
// render-pass caches are open-addressing maps whose entries move on
|
// render-pass caches are open-addressing maps whose entries move on
|
||||||
// insert, so no pointers into them are cached; the pipeline handle is
|
// insert, so no pointers into them are cached; the pipeline handle is
|
||||||
@@ -789,6 +836,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// re-resolve just the pipeline against the active pass; a change that
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
// flips it must fall back to the full path's pass selection.
|
// flips it must fall back to the full path's pass selection.
|
||||||
Bool drawUsesDepthStencil = false;
|
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};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||||
@@ -1079,7 +1131,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
// 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,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -1120,6 +1187,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool MaterializePendingClearForRenderbuffer(
|
Bool MaterializePendingClearForRenderbuffer(
|
||||||
VkCommandBuffer commandBuffer,
|
VkCommandBuffer commandBuffer,
|
||||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
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);
|
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::ITextureObject& texture,
|
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
|
// 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
|
// 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).
|
// 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, ...) \
|
#define VK_VERIFY(expr, ...) \
|
||||||
do { \
|
do { \
|
||||||
VkResult _vk_verify_result = (expr); \
|
VkResult _vk_verify_result = (expr); \
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
|||||||
|
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
add_subdirectory(Buffer)
|
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() {
|
EGLStateContext* GetState() {
|
||||||
if (!MG_State::pEGLContext) {
|
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();
|
return MG_State::pEGLContext.get();
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->DestroySurface(dpy, surface);
|
state->DestroySurface(dpy, surface);
|
||||||
return EGL_NO_SURFACE;
|
return EGL_NO_SURFACE;
|
||||||
}
|
}
|
||||||
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
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);
|
state->SetError(EGL_BAD_SURFACE);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
||||||
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (releaseCurrentRequest) {
|
if (releaseCurrentRequest) {
|
||||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
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->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
state->SetError(EGL_BAD_ACCESS);
|
state->SetError(EGL_BAD_ACCESS);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
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);
|
dpy, draw, read, ctx);
|
||||||
state->SetError(EGL_BAD_ACCESS);
|
state->SetError(EGL_BAD_ACCESS);
|
||||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||||
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
|
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
state->DestroySurface(dpy, surface);
|
state->DestroySurface(dpy, surface);
|
||||||
return EGL_NO_SURFACE;
|
return EGL_NO_SURFACE;
|
||||||
}
|
}
|
||||||
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
auto* backendObject = GetBackendObject(state);
|
auto* backendObject = GetBackendObject(state);
|
||||||
if (!backendObject) {
|
if (!backendObject) {
|
||||||
MGLOG_E("activeBackendObject not initialized!");
|
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
width = std::max<EGLint>(width, 1);
|
width = std::max<EGLint>(width, 1);
|
||||||
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||||
void* proc = MG_Impl::GetProcAddress(name);
|
void* proc = MG_Impl::GetProcAddress(name);
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
MGLOG_W("Failed to get function: %s", name);
|
MGLOG_D("Failed to get function: %s", name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return (__eglMustCastToProperFunctionPointerType)proc;
|
return (__eglMustCastToProperFunctionPointerType)proc;
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// quietly writing a differently-sized pattern.
|
// quietly writing a differently-sized pattern.
|
||||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||||
if (sourceSize != elementSize) {
|
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",
|
"converting between them is not implemented",
|
||||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
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"
|
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||||
// (a NULL buffers array, or a zero entry inside one).
|
// (a NULL buffers array, or a zero entry inside one).
|
||||||
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||||
const char* funcName) {
|
const char* funcName, Bool hasBuffer = true) {
|
||||||
if (size <= 0) {
|
if (hasBuffer && size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
@@ -1527,16 +1527,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
|
||||||
// offset and the size must be multiples of 4.
|
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
|
||||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
// 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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", funcName,
|
"MG_Impl/GLImpl", funcName,
|
||||||
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
|
||||||
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
|
||||||
offset, size)));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1548,7 +1559,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) 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()) {
|
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1732,10 +1748,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
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) {
|
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
||||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
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) {
|
const GLsizeiptr* sizes) {
|
||||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
|
||||||
if (!buffers || buffers[i] == 0) {
|
if (!buffers || buffers[i] == 0) {
|
||||||
BindBufferBase_State(target, first + i, 0);
|
BindBufferBase_State(target, first + i, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
#include "../Getter/GL_Getter.h"
|
#include "../Getter/GL_Getter.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
static Bool ValidateProgramForExecution(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
const char* functionName) {
|
||||||
if (!currentProgram) {
|
if (!currentProgram) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -34,10 +34,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
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) {
|
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) {
|
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -493,15 +500,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DispatchComputeIndirect(GLintptr indirect) {
|
void DispatchComputeIndirect(GLintptr indirect) {
|
||||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
// Argument and binding validation runs FIRST. Both are properties of the call and of GL
|
||||||
if (!dispatchComputeIndirect) {
|
// state, so a context whose backend cannot dispatch at all must still report the
|
||||||
MG_State::pGLContext->RecordError(
|
// argument error the spec names rather than masking every one of them with
|
||||||
ErrorCode::InvalidOperation,
|
// "unsupported" - which is what put GL_INVALID_OPERATION where
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
// KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
|
||||||
"Backend does not support indirect compute dispatch."));
|
//
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
|
||||||
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
// 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.
|
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||||
if (indirect < 0 || (indirect % 4) != 0) {
|
if (indirect < 0 || (indirect % 4) != 0) {
|
||||||
@@ -520,6 +524,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||||
return;
|
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);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,8 +607,80 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
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,
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
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;
|
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||||
if (!multiDrawElementsIndirectCount) {
|
if (!multiDrawElementsIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -595,6 +694,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
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;
|
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||||
if (!multiDrawArraysIndirectCount) {
|
if (!multiDrawArraysIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -1152,7 +1259,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||||
return;
|
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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
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_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||||
|
|
||||||
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||||
return (type)1; \
|
return (type)1; \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
#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__) {
|
#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, 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, 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, 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_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_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_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_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_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_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_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_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_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_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_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_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, 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, 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, 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)
|
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, 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, 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_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_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_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_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)
|
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, 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_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, 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_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, 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)
|
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, 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, 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, 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, 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, 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)
|
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, 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)
|
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) {
|
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
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) {
|
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) {
|
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||||
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
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) {
|
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||||
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
||||||
if (!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;
|
return;
|
||||||
}
|
}
|
||||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||||
@@ -620,6 +620,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
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) {
|
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||||
if (width < 0 || height < 0) {
|
if (width < 0 || height < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -641,7 +669,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, const char* caller) {
|
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, TextureInternalFormat format, const char* caller) {
|
||||||
if (samples < 0) {
|
if (samples < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -649,9 +677,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
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) {
|
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
|
// 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
|
// INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just
|
||||||
// cannot deliver it. Only a negative count is INVALID_VALUE.
|
// cannot deliver it. Only a negative count is INVALID_VALUE.
|
||||||
@@ -659,7 +688,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", caller,
|
"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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -684,7 +713,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
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;
|
if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return;
|
||||||
|
|
||||||
renderbufferObject->AllocateStorage({width, height});
|
renderbufferObject->AllocateStorage({width, height});
|
||||||
@@ -931,7 +960,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
|
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;
|
if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return;
|
||||||
|
|
||||||
renderbufferObject->AllocateStorage({width, height});
|
renderbufferObject->AllocateStorage({width, height});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.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/FramebufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
@@ -27,6 +28,11 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
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 {
|
namespace {
|
||||||
enum class IndexedBufferQueryKind {
|
enum class IndexedBufferQueryKind {
|
||||||
Binding,
|
Binding,
|
||||||
@@ -51,6 +57,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 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
|
// 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.
|
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
@@ -174,6 +189,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return frontendCount;
|
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) {
|
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
||||||
if (pname == GL_DRAW_BUFFER) {
|
if (pname == GL_DRAW_BUFFER) {
|
||||||
drawBufferIndex = 0;
|
drawBufferIndex = 0;
|
||||||
@@ -306,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||||
// scissor box and one depth range, so every in-range index answers with that single
|
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||||
// has no case for these, so routing them through it returned zeros.
|
// 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) {
|
Bool IsIndexedViewportQuery(GLenum target) {
|
||||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
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) {
|
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||||
GLint maxViewports = 0;
|
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
|
||||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||||
return false;
|
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) {
|
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||||
for (SizeT i = 0; i < count; ++i) {
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
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());
|
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return (GLubyte*)"Unknown";
|
return (GLubyte*)"Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +492,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return (GLubyte*)"Unknown";
|
return (GLubyte*)"Unknown";
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
@@ -596,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||||
return;
|
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_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||||
@@ -722,10 +816,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*data = 0;
|
*data = 0;
|
||||||
return;
|
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 Range1D range = bindingPoint.GetRange();
|
||||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
*data = static_cast<GLint>(range.end - range.start);
|
||||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
|
||||||
*data = static_cast<GLint>(end - start);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -755,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
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) {
|
switch (target) {
|
||||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||||
// viewport and one scissor box, so every in-range index reports that one.
|
|
||||||
case GL_VIEWPORT:
|
case GL_VIEWPORT:
|
||||||
case GL_SCISSOR_BOX:
|
case GL_SCISSOR_BOX:
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
|
}
|
||||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
// 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).
|
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||||
case GL_VERTEX_BINDING_BUFFER:
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
@@ -890,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -907,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (IsIndexedViewportQuery(target)) {
|
if (IsIndexedViewportQuery(target)) {
|
||||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
GLint ints[4] = {};
|
GLint ints[4] = {};
|
||||||
@@ -951,9 +1074,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*data = 0;
|
*data = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto start = std::min(range.start, bufferObject->GetSize());
|
// Verbatim, unclamped - see the GetIntegeri_v arm.
|
||||||
const auto end = std::min(range.end, bufferObject->GetSize());
|
*data = static_cast<GLint64>(range.end - range.start);
|
||||||
*data = static_cast<GLint64>(end - start);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -961,15 +1083,35 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto getInteger64i = MG_Backend::gBackendFunctionsTable.GL.GetInteger64i_v;
|
// The one indexed pname whose value genuinely needs 64 bits: a vertex buffer binding
|
||||||
if (!getInteger64i) {
|
// offset is an intptr, so taking the 32-bit route below would truncate it.
|
||||||
*data = 0;
|
if (target == GL_VERTEX_BINDING_OFFSET) {
|
||||||
MG_State::pGLContext->RecordError(
|
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
ErrorCode::InvalidOperation,
|
MG_State::pGLContext->RecordError(
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
|
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;
|
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) {
|
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||||
@@ -1379,7 +1521,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxCombinedAtomicCounters;
|
*params = kFrontendMaxCombinedAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||||
*params = kFrontendMaxCombinedUniformBlocks;
|
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
|
case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS:
|
||||||
*params = 1; // TODO
|
*params = 1; // TODO
|
||||||
@@ -1394,7 +1536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxFragmentAtomicCounters;
|
*params = kFrontendMaxFragmentAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxFragmentInputComponents;
|
*params = kFrontendMaxFragmentInputComponents;
|
||||||
@@ -1411,13 +1553,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxFragmentUniformVectors;
|
*params = kFrontendMaxFragmentUniformVectors;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
|
case GL_MAX_FRAGMENT_UNIFORM_BLOCKS:
|
||||||
*params = kFrontendMaxFragmentUniformBlocks;
|
*params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
|
case GL_MAX_GEOMETRY_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxGeometryAtomicCounters;
|
*params = kFrontendMaxGeometryAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxGeometryInputComponents;
|
*params = kFrontendMaxGeometryInputComponents;
|
||||||
@@ -1440,7 +1585,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
|
case GL_MAX_GEOMETRY_UNIFORM_BLOCKS:
|
||||||
*params = kFrontendMaxGeometryUniformBlocks;
|
*params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||||
*params = kFrontendMaxGeometryUniformComponents;
|
*params = kFrontendMaxGeometryUniformComponents;
|
||||||
@@ -1472,9 +1617,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
|
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxTessControlAtomicCounters;
|
*params = kFrontendMaxTessControlAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxTessControlAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
|
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxTessEvaluationAtomicCounters;
|
*params = kFrontendMaxTessEvaluationAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxTessEvaluationAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
|
case GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS:
|
||||||
*params = 0;
|
*params = 0;
|
||||||
return;
|
return;
|
||||||
@@ -1482,10 +1633,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = 0;
|
*params = 0;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||||
*params = 15; // TODO
|
*params = 15; // TODO
|
||||||
@@ -1502,13 +1653,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
|
case GL_MAX_VERTEX_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxVertexAtomicCounters;
|
*params = kFrontendMaxVertexAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxVertexAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||||
*params = MG_Backend::pActiveBackendObject
|
*params = MG_Backend::pActiveBackendObject
|
||||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||||
*params = kFrontendMaxVertexUniformComponents;
|
*params = kFrontendMaxVertexUniformComponents;
|
||||||
@@ -1520,7 +1674,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxVertexOutputComponents;
|
*params = kFrontendMaxVertexOutputComponents;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_UNIFORM_BLOCKS:
|
case GL_MAX_VERTEX_UNIFORM_BLOCKS:
|
||||||
*params = kFrontendMaxVertexUniformBlocks;
|
*params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks);
|
||||||
return;
|
return;
|
||||||
case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
|
case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
|
||||||
*params = 0; // compressed texture upload entrypoints are still unimplemented
|
*params = 0; // compressed texture upload entrypoints are still unimplemented
|
||||||
@@ -1891,7 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
@@ -1920,13 +2074,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
|
*params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS:
|
||||||
*params = dynamicParameters.MaxComputeShaderStorageBlocks;
|
*params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks);
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS:
|
||||||
*params = dynamicParameters.MaxCombinedShaderStorageBlocks;
|
*params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks);
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
|
case GL_MAX_COMPUTE_UNIFORM_BLOCKS:
|
||||||
*params = dynamicParameters.MaxComputeUniformBlocks;
|
*params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks);
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
|
case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS:
|
||||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||||
@@ -2056,7 +2210,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
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;
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
@@ -2121,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.MaxViewportHeight;
|
params[1] = dynamicParameters.MaxViewportHeight;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_VIEWPORTS:
|
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;
|
break;
|
||||||
case GL_MINOR_VERSION:
|
case GL_MINOR_VERSION:
|
||||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||||
@@ -2177,7 +2347,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||||
break;
|
break;
|
||||||
default:
|
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,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
||||||
std::format("Invalid enum: 0x{:X}", pname)));
|
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());
|
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) {
|
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
if (!shaderObject) return;
|
||||||
@@ -756,9 +771,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = shaderObject->GetDeleteStatus();
|
*params = shaderObject->GetDeleteStatus();
|
||||||
break;
|
break;
|
||||||
case GL_COMPILE_STATUS:
|
case GL_COMPILE_STATUS:
|
||||||
|
if (AnswerCompileOptimistically(shaderObject)) {
|
||||||
|
*params = GL_TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
*params = shaderObject->GetCompileStatus();
|
*params = shaderObject->GetCompileStatus();
|
||||||
break;
|
break;
|
||||||
case GL_INFO_LOG_LENGTH:
|
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;
|
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||||
break;
|
break;
|
||||||
case GL_SHADER_SOURCE_LENGTH:
|
case GL_SHADER_SOURCE_LENGTH:
|
||||||
@@ -784,6 +810,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
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();
|
const auto& log = shaderObject->GetInfoLog();
|
||||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
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
|
// 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
|
// 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
|
// 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) {
|
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||||
const Int columns = ttype->getMatrixCols();
|
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
|
// 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) {
|
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||||
if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) {
|
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||||
return static_cast<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
|
|
||||||
}
|
|
||||||
return tightSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||||
@@ -869,13 +908,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
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);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
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
|
// TODO: handle 1i variant as texture unit
|
||||||
@@ -917,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
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);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -926,22 +971,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A double-precision uniform is the one case where the stored component type can
|
// A double-precision uniform is the one case where the stored component type differs
|
||||||
// differ from the queried one for a non-opaque uniform, and the difference is not
|
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||||
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
|
// narrowed to 32 bits before the module reaches a backend
|
||||||
// caller's buffer as well as return nonsense. Read component by component and let
|
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||||
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
|
// 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) {
|
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||||
// The slot the linker handed out is exactly `columns` columns wide, so it also
|
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||||
// states the column stride - which for a double matrix is not a float's 16 bytes.
|
// tightly packed run and never reaches the stride at all.
|
||||||
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
|
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
for (Int row = 0; row < rows; ++row) {
|
for (Int row = 0; row < rows; ++row) {
|
||||||
GLdouble component = 0.0;
|
GLfloat component = 0.0f;
|
||||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
|
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||||
sizeof(component));
|
sizeof(component));
|
||||||
if constexpr (std::is_integral_v<T>) {
|
if constexpr (std::is_integral_v<T>) {
|
||||||
// Rounded to the nearest integer and clamped into the queried type's
|
// Rounded to the nearest integer and clamped into the queried type's
|
||||||
@@ -1012,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MGLOG_E("activeBackendObject is not initialized!");
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
@@ -1085,23 +1135,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||||
location, programObject.GetMaxUniformLocation());
|
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 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);
|
SizeT writeSize = ItemCount * sizeof(T);
|
||||||
if (size < writeSize) {
|
if (size < writeSize) {
|
||||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
// 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",
|
"bytes; clamping",
|
||||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||||
writeSize = 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 ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||||
// Should not happen: linking gives every settable uniform backing
|
// Should not happen: linking gives every settable uniform backing
|
||||||
// storage. Log and drop the write instead of faulting.
|
// 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",
|
"uboSize=%zu); dropping write",
|
||||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||||
writeSize, uboSize);
|
writeSize, uboSize);
|
||||||
@@ -1192,36 +1264,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
|
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||||
// upload template - it is already typed on the component - but a matrix does: the
|
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||||
// column stride the linker used for a double matrix is not the 16 bytes a float one
|
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||||
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
|
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||||
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
|
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||||
template <typename Program>
|
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||||
const GLdouble* value, Int columns, Int rows) {
|
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||||
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
|
//
|
||||||
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
|
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||||
Vector<GLdouble> column(static_cast<SizeT>(rows));
|
template <GLsizei ItemCount>
|
||||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
if (value == nullptr || count <= 0) {
|
||||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
// Same shape as the float entry points: the location validation still runs, and a
|
||||||
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
|
// null pointer is left to fault exactly where glUniform*fv would.
|
||||||
return;
|
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||||
}
|
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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).
|
// 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
|
// Helper function to transpose a 2x2 matrix
|
||||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||||
// Input matrix is in column-major order (OpenGL default)
|
// Input matrix is in column-major order (OpenGL default)
|
||||||
@@ -1716,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
@@ -2033,71 +2124,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
void Uniform1d(GLint location, GLdouble v0) {
|
void Uniform1d(GLint location, GLdouble v0) {
|
||||||
const GLdouble v[] = {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) {
|
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) {
|
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
|
||||||
const GLdouble v[] = {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) {
|
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) {
|
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
|
||||||
const GLdouble v[] = {v0, 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) {
|
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) {
|
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
|
||||||
const GLdouble v[] = {v0, 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) {
|
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) {
|
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||||
const GLdouble v[] = {v0, v1, 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) {
|
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) {
|
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||||
const GLdouble v[] = {v0, v1, 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) {
|
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) {
|
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||||
const GLdouble v[] = {v0, v1, v2, 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) {
|
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) {
|
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||||
const GLdouble v[] = {v0, v1, v2, 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) {
|
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) {
|
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
if (location == -1) return;
|
if (location == -1) return;
|
||||||
@@ -2658,6 +2749,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
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__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||||
|
|||||||
@@ -19,16 +19,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
|
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
|
// GL 4.6 core 7.4 asks only that the name came from GenProgramPipelines and has not been
|
||||||
// reserved-but-unmaterialised name is INVALID_OPERATION (GL 4.6 core 7.4).
|
// 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 SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
|
||||||
const char* function) {
|
const char* function) {
|
||||||
if (!MG_State::pGLContext->IsProgramPipelineObject(pipeline)) {
|
const auto& object = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
|
||||||
|
if (!object) {
|
||||||
RecordPipelineError(ErrorCode::InvalidOperation, function,
|
RecordPipelineError(ErrorCode::InvalidOperation, function,
|
||||||
std::format("Program pipeline {} does not exist.", pipeline));
|
std::format("Program pipeline {} does not exist.", pipeline));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &MG_State::pGLContext->GetProgramPipelineObject(pipeline);
|
return &object;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidatePipelineCount(GLsizei n, const char* function) {
|
Bool ValidatePipelineCount(GLsizei n, const char* function) {
|
||||||
|
|||||||
@@ -210,11 +210,66 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
|
|
||||||
// ---- model construction --------------------------------------------------------
|
// ---- 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,
|
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
blockInterfaceIndex.assign(blockCount, -1);
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
|
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||||
|
|
||||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(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));
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||||
if (tIndex >= 0 && tIndex < blockCount) {
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
resource.stages =
|
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
||||||
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
|
stagesFromMembers, tIndex);
|
||||||
}
|
}
|
||||||
model.uniformBlocks.push_back(Move(resource));
|
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) {
|
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
|
|
||||||
@@ -359,6 +425,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
for (Int index = 0; index < inputCount; ++index) {
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeInput(index);
|
const auto& refl = mutableReflection.getPipeInput(index);
|
||||||
const glslang::TType* type = refl.getType();
|
const glslang::TType* type = refl.getType();
|
||||||
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
// names; GL enumerates the GL spellings.
|
// names; GL enumerates the GL spellings.
|
||||||
@@ -373,18 +440,28 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
model.programInputs.push_back(Move(resource));
|
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();
|
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||||
const glslang::TType* type = refl.getType();
|
const glslang::TType* type = refl.getType();
|
||||||
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = WithArraySuffix(refl.name, type);
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
if (resource.location < 0) {
|
if (resource.location < 0 || !lastStageIsFragment) {
|
||||||
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
|
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||||
// output both have no location, and therefore no color index either.
|
// non-fragment stage's outputs have no color number at all - either way there
|
||||||
|
// is no color index.
|
||||||
resource.locationIndex = -1;
|
resource.locationIndex = -1;
|
||||||
} else {
|
} else {
|
||||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_RenderState.h"
|
#include "GL_RenderState.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.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);
|
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||||
if (target != GL_BLEND) {
|
// 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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
"MG_Impl/GLImpl", functionName,
|
String(indexName) + " index " + std::to_string(index) +
|
||||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
"."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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,
|
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||||
::MobileGL::BlendEquation& outEquation) {
|
::MobileGL::BlendEquation& outEquation) {
|
||||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
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) {
|
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
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) {
|
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
Bool applyFront = false;
|
Bool applyFront = false;
|
||||||
Bool applyBack = false;
|
Bool applyBack = false;
|
||||||
@@ -174,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||||
if (width < 0 || height < 0) {
|
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
|
||||||
"Width abd height must be non-negative."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
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) {
|
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +520,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
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) {
|
GLboolean IsEnabled_State(GLenum cap) {
|
||||||
@@ -713,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Disablei_State(GLenum target, GLuint index) {
|
void Disablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Enablei_State(GLenum target, GLuint index) {
|
void Enablei_State(GLenum target, GLuint index) {
|
||||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Viewport_State(x, y, width, height);
|
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) {
|
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void Enablei(GLenum target, GLuint index);
|
void Enablei(GLenum target, GLuint index);
|
||||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
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 StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "GL_Sampler.h"
|
#include "GL_Sampler.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
#include "../Getter/GL_Getter.h"
|
#include "../Getter/GL_Getter.h"
|
||||||
|
#include "../Texture/GL_Texture.h"
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/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
|
// The number of texture units a sampler may be bound to is the same count a TEXTURE may be
|
||||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
|
// bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
|
||||||
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
|
// computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
|
||||||
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
|
// readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
|
||||||
// matter what the driver reports.
|
// come to disagree about which units exist.
|
||||||
static GLint GetSamplerBindableTextureUnitCount() {
|
static GLint GetSamplerBindableTextureUnitCount() {
|
||||||
GLint maxTextureUnits = 0;
|
return GetCombinedTextureImageUnitCount();
|
||||||
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
|
||||||
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||||
@@ -336,8 +335,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
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) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -613,6 +613,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
"Compressed texture formats are not supported."));
|
"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
|
} // namespace
|
||||||
|
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
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::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||||
if (readBytesPerTexel != bytesPerTexel) {
|
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);
|
bytesPerTexel, readBytesPerTexel);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1359,6 +1376,47 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
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,
|
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) {
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||||
@@ -1432,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
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);
|
free(processedPixels);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1541,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
{width, height, 1}, false, inputSize);
|
{width, height, 1}, false, inputSize);
|
||||||
|
|
||||||
if (!processedPixels || inputSize == 0) {
|
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);
|
height);
|
||||||
if (processedPixels) free(processedPixels);
|
if (processedPixels) free(processedPixels);
|
||||||
return;
|
return;
|
||||||
@@ -1555,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
|
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);
|
free(processedPixels);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2106,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (processedPixels && imageSize > 0) {
|
if (processedPixels && imageSize > 0) {
|
||||||
if (imageSize != internalBytes) {
|
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.",
|
"This may indicate an alignment or processing issue.",
|
||||||
__func__, imageSize, internalBytes);
|
__func__, imageSize, internalBytes);
|
||||||
}
|
}
|
||||||
@@ -2221,6 +2279,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||||
{{width, height, 1}, internalBytes});
|
{{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) {
|
if (!originalPixels) {
|
||||||
@@ -2235,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (processedPixels && imageSize > 0) {
|
if (processedPixels && imageSize > 0) {
|
||||||
if (imageSize != internalBytes) {
|
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.",
|
"This may indicate an alignment or processing issue.",
|
||||||
imageSize, internalBytes);
|
imageSize, internalBytes);
|
||||||
}
|
}
|
||||||
@@ -2910,7 +2985,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2924,7 +3000,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2938,16 +3015,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||||
if (params) {
|
if (params) {
|
||||||
// A level stored compressed must report the token it was given, not the
|
// 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
|
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||||
// internalformat - still answers with its resolved storage format.
|
// every other level answers with its resolved storage format.
|
||||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||||
*params = (compressedFormat != GL_NONE)
|
*params = (compressedFormat != GL_NONE)
|
||||||
? (GLint)compressedFormat
|
? (GLint)compressedFormat
|
||||||
@@ -3045,7 +3123,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3059,7 +3138,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3073,16 +3153,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||||
if (params) {
|
if (params) {
|
||||||
// A level stored compressed must report the token it was given, not the
|
// 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
|
// uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
|
||||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
// that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
|
||||||
// internalformat - still answers with its resolved storage format.
|
// every other level answers with its resolved storage format.
|
||||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||||
*params = (GLfloat)((compressedFormat != GL_NONE)
|
*params = (GLfloat)((compressedFormat != GL_NONE)
|
||||||
? compressedFormat
|
? compressedFormat
|
||||||
@@ -3301,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
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,
|
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,
|
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) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||||
|
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||||
@@ -3315,14 +3468,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||||
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
|
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
|
||||||
return false;
|
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) {
|
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -3333,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||||
return false;
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -3403,7 +3609,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
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 outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
||||||
GLenum realInternalFormat = GL_RGBA8;
|
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,
|
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||||
GLint border) {
|
GLint border) {
|
||||||
// TODO: implement
|
// 1D textures are not implemented by this backend set. Record the error the way every
|
||||||
THROW_UNIMPL_EXCEPTION;
|
// 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,
|
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
@@ -3437,10 +3651,170 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
RecordUnsupportedCompressedFormat(__func__);
|
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,
|
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
// ======================= Converting ================================
|
||||||
RecordUnsupportedCompressedFormat(__func__);
|
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,
|
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.
|
// SetMipmapCompressedImage re-arms it.
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
|
||||||
|
|
||||||
const void* compressedBytes = data;
|
if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
|
||||||
const auto& pixelUnpackBufferObject =
|
const void* compressedBytes = CompressedUnpackSource(data);
|
||||||
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;
|
|
||||||
}
|
|
||||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
|
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
|
||||||
expectedImageSize);
|
expectedImageSize);
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
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
|
// 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
|
// 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.
|
// 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 (const auto uploadTarget : textureObject->GetUploadTargets()) {
|
||||||
for (GLsizei level = 0; level < levels; ++level) {
|
for (GLsizei level = 0; level < levels; ++level) {
|
||||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> 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;
|
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
|
||||||
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
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.
|
// See TextureStorage1D.
|
||||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||||
@@ -4079,10 +4447,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
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,
|
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
GLsizei depth) {
|
GLsizei depth) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
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);
|
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
||||||
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
||||||
@@ -4405,6 +4809,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
free(processedPixels);
|
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,
|
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) {
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
@@ -4482,6 +4894,132 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
|
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) {
|
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
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,
|
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,
|
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
// SharedPtr, and let the validator record the error this entry point owes.
|
||||||
srcWidth, srcHeight, srcDepth)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
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);
|
GLenum format, GLenum type, const void* pixels);
|
||||||
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
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);
|
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 TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||||
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
|
||||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param);
|
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,
|
void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||||
GLsizei imageSize, const void* data);
|
GLsizei imageSize, const void* data);
|
||||||
void BindTexture(GLenum target, GLuint texture);
|
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);
|
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
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||||
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||||
Bool ValidateTextureTarget(TextureTarget target) {
|
Bool ValidateTextureTarget(TextureTarget target) {
|
||||||
@@ -353,6 +354,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
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) {
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||||
if (!textureObject) {
|
if (!textureObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -424,19 +482,141 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
namespace {
|
||||||
auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
// Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6
|
||||||
auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
// reasons about. Colour components are independent bits so "subset" is a plain
|
||||||
if (unsizedFormat1 != unsizedFormat2) {
|
// 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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||||
"The base internal format of the two formats do not match ({} vs. {})",
|
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(),
|
"formats are not copy-compatible.",
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str())));
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
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
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureInternalFormat internalFormat,
|
TextureInternalFormat internalFormat,
|
||||||
TexturePixelDataType type);
|
TexturePixelDataType type);
|
||||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
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);
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
// 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
|
// 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);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
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);
|
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
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -179,6 +179,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return vao;
|
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) {
|
static bool ValidateVertexAttribPname(GLenum pname) {
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||||
@@ -293,9 +315,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
auto offset = reinterpret_cast<SizeT>(pointer);
|
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->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,
|
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.
|
// backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver.
|
||||||
const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
||||||
const int effectiveSize = isBgra ? 4 : size;
|
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->BindAttributeBuffer(index, vbo);
|
||||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
|
vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexArray_State(GLuint array) {
|
void BindVertexArray_State(GLuint array) {
|
||||||
@@ -502,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (!MG_Backend::pActiveBackendObject ||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
!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 "
|
"backend has no double-precision vertex attribute support - see the "
|
||||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||||
attribindex);
|
attribindex);
|
||||||
@@ -944,7 +969,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLfloat>(attr->Size);
|
params[0] = static_cast<GLfloat>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLfloat>(attr->Stride);
|
params[0] = static_cast<GLfloat>(attr->LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->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);
|
params[0] = static_cast<GLdouble>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLdouble>(attr->Stride);
|
params[0] = static_cast<GLdouble>(attr->LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->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:
|
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||||
params[0] = attr->Size;
|
params[0] = attr->Size;
|
||||||
return;
|
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:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = attr->Stride;
|
params[0] = attr->LegacyStride;
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->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);
|
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) {
|
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
||||||
@@ -1222,7 +1250,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*param = static_cast<GLint>(attr.Size);
|
*param = static_cast<GLint>(attr.Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
*param = static_cast<GLint>(attr.Stride);
|
*param = static_cast<GLint>(attr.LegacyStride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.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) {
|
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
||||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer");
|
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||||
const GLsizei* strides) {
|
const GLsizei* strides) {
|
||||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
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) {
|
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
||||||
"VertexAttribFormat");
|
"VertexAttribFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
||||||
"VertexAttribIFormat");
|
"VertexAttribIFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
}
|
}
|
||||||
@@ -1341,7 +1369,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
||||||
@@ -1349,7 +1377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor");
|
auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
||||||
vao->SetBindingDivisor(bindingindex, divisor);
|
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.
|
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
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) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||||
|
|
||||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
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) {}
|
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"));
|
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
||||||
}
|
}
|
||||||
if (!fns->Valid()) {
|
if (!fns->Valid()) {
|
||||||
MGLOG_E("glx: failed to load libX11 entry points");
|
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
||||||
}
|
}
|
||||||
return fns;
|
return fns;
|
||||||
}();
|
}();
|
||||||
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
Uint32 width = 0;
|
Uint32 width = 0;
|
||||||
Uint32 height = 0;
|
Uint32 height = 0;
|
||||||
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||||
if (surface == EGL_NO_SURFACE) {
|
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);
|
width, height);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||||
EGLDisplay display = EnsureDisplay();
|
EGLDisplay display = EnsureDisplay();
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
MGLOG_E("glx: no EGL display");
|
MGLOG_E_ONCE("glx: no EGL display");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||||
@@ -376,13 +376,13 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
||||||
configCount <= 0) {
|
configCount <= 0) {
|
||||||
MGLOG_E("glx: eglChooseConfig failed");
|
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||||
if (eglContext == EGL_NO_CONTEXT) {
|
if (eglContext == EGL_NO_CONTEXT) {
|
||||||
MGLOG_E("glx: eglCreateContext failed");
|
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,7 +931,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
|
|
||||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
||||||
object->Context)) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
t_current = {dpy, drawable, drawable, context};
|
t_current = {dpy, drawable, drawable, context};
|
||||||
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
if (context && draw != read) {
|
if (context && draw != read) {
|
||||||
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
||||||
// drawable for both, which is what every real caller here needs.
|
// 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);
|
read);
|
||||||
}
|
}
|
||||||
const int result = MakeCurrent(dpy, draw, context);
|
const int result = MakeCurrent(dpy, draw, context);
|
||||||
@@ -958,7 +958,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
auto& surfaces = DrawableSurfaces();
|
auto& surfaces = DrawableSurfaces();
|
||||||
auto it = surfaces.find(drawable);
|
auto it = surfaces.find(drawable);
|
||||||
if (it == surfaces.end()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
SyncSurfaceSize(dpy, drawable, it->second);
|
SyncSurfaceSize(dpy, drawable, it->second);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
|
|||||||
#endif
|
#endif
|
||||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
MGLOG_D("Failed to get function: %s", (const char*)name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
|
|||||||
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
||||||
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
||||||
|
|
||||||
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
|
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Impl
|
} // namespace MobileGL::MG_Impl
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
}
|
}
|
||||||
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
||||||
if (!metalLayerClass) {
|
if (!metalLayerClass) {
|
||||||
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
|
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
static_cast<GLint>(geometry.DrawableSize.width),
|
static_cast<GLint>(geometry.DrawableSize.width),
|
||||||
static_cast<GLint>(geometry.DrawableSize.height));
|
static_cast<GLint>(geometry.DrawableSize.height));
|
||||||
if (error != kCGLNoError) {
|
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);
|
const auto error = CGLImpl::SetCurrentContext(context);
|
||||||
if (error != kCGLNoError) {
|
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);
|
const auto error = CGLImpl::FlushDrawable(context);
|
||||||
if (error != kCGLNoError) {
|
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.width),
|
||||||
static_cast<GLint>(geometry.DrawableSize.height));
|
static_cast<GLint>(geometry.DrawableSize.height));
|
||||||
if (error != kCGLNoError) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
CGLImpl::UpdateContext(context);
|
CGLImpl::UpdateContext(context);
|
||||||
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
SEL selector = sel_registerName(selectorName);
|
SEL selector = sel_registerName(selectorName);
|
||||||
Method method = class_getInstanceMethod(cls, selector);
|
Method method = class_getInstanceMethod(cls, selector);
|
||||||
if (!method) {
|
if (!method) {
|
||||||
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
|
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (original) {
|
if (original) {
|
||||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
SEL selector = sel_registerName(selectorName);
|
SEL selector = sel_registerName(selectorName);
|
||||||
Method method = class_getClassMethod(cls, selector);
|
Method method = class_getClassMethod(cls, selector);
|
||||||
if (!method) {
|
if (!method) {
|
||||||
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
|
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
method_setImplementation(method, replacement);
|
method_setImplementation(method, replacement);
|
||||||
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||||
if (!pixelFormatClass || !contextClass) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
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);
|
SetLastError(ERROR_NOT_SUPPORTED);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
|||||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
LPGLYPHMETRICSFLOAT) {
|
LPGLYPHMETRICSFLOAT) {
|
||||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
LPGLYPHMETRICSFLOAT) {
|
LPGLYPHMETRICSFLOAT) {
|
||||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
Uint32 width = 0;
|
Uint32 width = 0;
|
||||||
Uint32 height = 0;
|
Uint32 height = 0;
|
||||||
if (!QueryClientSize(hwnd, width, height)) {
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
EGLSurface surface =
|
EGLSurface surface =
|
||||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||||
if (surface == EGL_NO_SURFACE) {
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||||
EGLDisplay display = EnsureDisplay();
|
EGLDisplay display = EnsureDisplay();
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
MGLOG_E("wgl: no EGL display");
|
MGLOG_E_ONCE("wgl: no EGL display");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
EGLConfig config = nullptr;
|
EGLConfig config = nullptr;
|
||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || 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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||||
if (eglContext == EGL_NO_CONTEXT) {
|
if (eglContext == EGL_NO_CONTEXT) {
|
||||||
MGLOG_E("wgl: eglCreateContext failed");
|
MGLOG_E_ONCE("wgl: eglCreateContext failed");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
auto& surfaces = WindowSurfaces();
|
auto& surfaces = WindowSurfaces();
|
||||||
auto it = surfaces.find(hwnd);
|
auto it = surfaces.find(hwnd);
|
||||||
if (it == surfaces.end()) {
|
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;
|
return FALSE;
|
||||||
}
|
}
|
||||||
SyncSurfaceSize(hwnd, it->second);
|
SyncSurfaceSize(hwnd, it->second);
|
||||||
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
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;
|
return FALSE;
|
||||||
}
|
}
|
||||||
t_current = {hdc, hglrc};
|
t_current = {hdc, hglrc};
|
||||||
|
|||||||
@@ -50,9 +50,38 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/CrossFrameBufferScenario.cpp
|
Scenarios/CrossFrameBufferScenario.cpp
|
||||||
Scenarios/ResidentIndexScenario.cpp
|
Scenarios/ResidentIndexScenario.cpp
|
||||||
Scenarios/MultiDrawScenario.cpp
|
Scenarios/MultiDrawScenario.cpp
|
||||||
|
Scenarios/DrawParametersScenario.cpp
|
||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.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
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
@@ -169,25 +198,24 @@ endif()
|
|||||||
option(MOBILEGL_ITEST_REQUIRE_GPU
|
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||||
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
"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
|
# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless
|
||||||
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a
|
||||||
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
# developer's machine and a CI runner take the SAME path whether or not a window
|
||||||
# fails and every scenario skips.
|
# system happens to be running. This used to inject "x11", which is how the lane
|
||||||
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
# came up green on a workstation with WSLg and died on a runner with no X server.
|
||||||
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
#
|
||||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
# A build-system knob would not just be redundant, it would be a trap: `set(...
|
||||||
else()
|
# CACHE ...)` does not rewrite an existing cache, so every build directory
|
||||||
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
# configured before this change would keep injecting EGL_PLATFORM=x11 and go on
|
||||||
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
# binding to a window system - silently, and only on the machines that have one.
|
||||||
endif()
|
# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own
|
||||||
|
# environment, which the harness still honours.
|
||||||
|
|
||||||
set(MGL_ITEST_COMMON_ENV "")
|
set(MGL_ITEST_COMMON_ENV "")
|
||||||
if (MOBILEGL_ITEST_EGL_VENDOR)
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
endif()
|
endif()
|
||||||
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11
|
||||||
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
|
||||||
endif()
|
|
||||||
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||||
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||||
endif()
|
endif()
|
||||||
@@ -221,6 +249,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
|||||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
"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.
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
set(MGL_ITEST_TIMEOUT 120)
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
@@ -268,3 +298,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
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;
|
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
|
// 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
|
// literally the same sequence - a pre-flight that tests something narrower
|
||||||
// than what the parent will do is exactly the kind of "predictive" check
|
// 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
|
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||||
// fills outReason either way.
|
// fills outReason either way.
|
||||||
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
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);
|
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||||
@@ -199,11 +236,10 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
close(channel[0]);
|
close(channel[0]);
|
||||||
// The child is EXPECTED to die on a signal on an unusable
|
// No core suppression here, deliberately: when the child dies on a
|
||||||
// platform; that is the measurement. Do not let each such
|
// signal, the core IS the diagnosis (an rlimit that used to sit here
|
||||||
// measurement drop a core file next to the test binary.
|
// made a CI-only crash undebuggable). Machines that do not want
|
||||||
const rlimit noCore{0, 0};
|
// cores control that with the usual ulimit/core_pattern knobs.
|
||||||
setrlimit(RLIMIT_CORE, &noCore);
|
|
||||||
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||||
EglBringUp local;
|
EglBringUp local;
|
||||||
std::string reason;
|
std::string reason;
|
||||||
@@ -284,9 +320,19 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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() {
|
bool RequireGpu() {
|
||||||
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
}
|
||||||
|
|
||||||
|
bool RequireHardwareGpu() {
|
||||||
|
return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||||
@@ -390,6 +436,10 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HeadlessGL::HeadlessGL() {
|
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_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||||
m_usable = BringUp();
|
m_usable = BringUp();
|
||||||
}
|
}
|
||||||
@@ -551,9 +601,13 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Image ReadPixels(int width, int height) {
|
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);
|
Image image(width, height);
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
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;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ namespace MGITest {
|
|||||||
// a job that ran everything.
|
// a job that ran everything.
|
||||||
bool RequireGpu();
|
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 {
|
struct Rgba8 {
|
||||||
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
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);
|
void ClearTo(float r, float g, float b, float a);
|
||||||
|
|
||||||
// Reads back the whole currently bound READ framebuffer. width/height must
|
// Reads back the whole currently bound READ framebuffer.
|
||||||
// be the target's full size - DirectVulkan's default-framebuffer readback
|
|
||||||
// only re-orients a full-extent read.
|
|
||||||
Image ReadPixels(int width, int height);
|
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.
|
// Drains any GL error queue and returns the first error, or 0.
|
||||||
unsigned int FirstGLError();
|
unsigned int FirstGLError();
|
||||||
const char* GLErrorName(unsigned int error);
|
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();
|
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||||
}
|
}
|
||||||
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||||
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
||||||
// a misconfigured vendor pin silently lands on the software
|
// unusable harness is a failure, not a silent skip" - it is the
|
||||||
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
// falsifiability switch, and CI is exactly where it belongs. But CI
|
||||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
// runners have no GPU, so folding "must not be llvmpipe" into the
|
||||||
<< gl.RendererString();
|
// 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
|
// A scenario starts from a clean slate but shares the context (and so
|
||||||
// the renderer's memos) with every other scenario in this process -
|
// the renderer's memos) with every other scenario in this process -
|
||||||
|
|||||||
@@ -26,8 +26,14 @@ namespace {
|
|||||||
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||||
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||||
if (gl.Usable()) {
|
if (gl.Usable()) {
|
||||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n",
|
// EGL_PLATFORM is echoed because it is the invariant this harness
|
||||||
gl.RendererString().c_str(), gl.Width(), gl.Height());
|
// 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()) {
|
} else if (MGITest::RequireGpu()) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
" 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;
|
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
|
// 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.
|
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||||
class CompilerThreadScope {
|
class CompilerThreadScope {
|
||||||
@@ -463,5 +479,81 @@ void main() {
|
|||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
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
|
||||||
} // namespace MGITest
|
} // 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
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageLoadStoreSsoScenario.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 - IMAGE UNIFORMS REACHED THROUGH A PROGRAM PIPELINE.
|
||||||
|
//
|
||||||
|
// KHR-GL42.shader_image_load_store.advanced-sso-simple reduced to its mechanism. An ARRAY of
|
||||||
|
// image uniforms lives in a separable FRAGMENT program; the application assigns each element its
|
||||||
|
// own image unit with glProgramUniform1i, on a program that is not current and whose pipeline is
|
||||||
|
// not even bound yet; the draw then goes through the pipeline, i.e. through the flattened
|
||||||
|
// composite program (MG_State/GLState/Core.cpp, GetProgramForDraw) rather than through the stage
|
||||||
|
// program the units were written to.
|
||||||
|
//
|
||||||
|
// Three separate things have to survive that indirection, and each one is a different mechanism:
|
||||||
|
//
|
||||||
|
// 1. the units themselves, which are per-program state on a DIFFERENT object from the one the
|
||||||
|
// draw reads (the composite mirror carries them);
|
||||||
|
// 2. the units as seen by a backend that cannot take them at draw time - Espryt has to BAKE an
|
||||||
|
// image unit into the ESSL it generates, because ES forbids glUniform1i on image uniforms,
|
||||||
|
// so a change has to invalidate the generated program;
|
||||||
|
// 3. per-ELEMENT assignment, which is what makes this different from every sampler case: the
|
||||||
|
// four elements of g_image[] are four locations with four different units, and nothing may
|
||||||
|
// collapse them to the array's base.
|
||||||
|
//
|
||||||
|
// Two pipelines that SHARE their vertex stage program and differ only in the fragment one are
|
||||||
|
// used exactly as the conformance case does, because that is what makes the composite cache and
|
||||||
|
// the stage programs' separate uniform storage both load-bearing at once.
|
||||||
|
|
||||||
|
#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* kSsoVS = R"(#version 420 core
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
case 3: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The conformance case's two fragment programs: one with an explicit format qualifier,
|
||||||
|
// one writeonly with none. Both write every element of a four-image array and discard.
|
||||||
|
constexpr const char* kImageFS0 = R"(#version 420 core
|
||||||
|
layout(rgba32f) uniform image2D g_image[4];
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < g_image.length(); ++i) {
|
||||||
|
imageStore(g_image[i], ivec2(gl_FragCoord), vec4(1.0));
|
||||||
|
}
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kImageFS1 = R"(#version 420 core
|
||||||
|
writeonly uniform image2D g_image[4];
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < g_image.length(); ++i) {
|
||||||
|
imageStore(g_image[i], ivec2(gl_FragCoord), vec4(2.0));
|
||||||
|
}
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ImageLoadStoreSsoScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindProgramPipeline(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||||
|
for (GLuint p : m_pipelines) glDeleteProgramPipelines(1, &p);
|
||||||
|
m_programs.clear();
|
||||||
|
m_pipelines.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeSeparable(GLenum stage, const char* source) {
|
||||||
|
const GLuint program = glCreateShaderProgramv(stage, 1, &source);
|
||||||
|
if (program != 0) m_programs.push_back(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u)
|
||||||
|
<< "glCreateShaderProgramv(stage 0x" << std::hex << stage << std::dec << ") left a GL error";
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "glCreateShaderProgramv(stage 0x" << std::hex << stage << std::dec
|
||||||
|
<< ") did not link: " << log;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakePipeline() {
|
||||||
|
GLuint pipeline = 0;
|
||||||
|
glGenProgramPipelines(1, &pipeline);
|
||||||
|
m_pipelines.push_back(pipeline);
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Espryt reaches the GPU through an ES driver, and ES forbids glUniform1i on an
|
||||||
|
// image uniform: the unit has to be BAKED into the generated ESSL as
|
||||||
|
// layout(binding = N) (RebindImageUniformsToFrontendUnits, MG_Backend/DirectGLES).
|
||||||
|
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||||
|
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||||
|
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||||
|
// spelling in a single declaration and cannot be expressed at all without splitting
|
||||||
|
// the array into one declaration per element and rewriting every use of it.
|
||||||
|
//
|
||||||
|
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
||||||
|
// storage-block rebinding cases: the defect is per-backend and the frontend
|
||||||
|
// mechanism these cases exist for - per-element units surviving the trip to the
|
||||||
|
// pipeline composite - is fully exercised on Magma.
|
||||||
|
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||||
|
|
||||||
|
// The scenarios below need image load/store at all; a driver without it should skip
|
||||||
|
// rather than fail.
|
||||||
|
bool ImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::vector<GLuint> m_pipelines;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The whole conformance shape in one case: two pipelines sharing a vertex stage, four image
|
||||||
|
// array elements each pointed at a different unit through glProgramUniform1i, eight layers of
|
||||||
|
// one array texture bound one per unit, and every layer checked.
|
||||||
|
//
|
||||||
|
// Layers alternate 1.0 / 2.0 because the two fragment programs interleave their units
|
||||||
|
// (0,2,4,6 and 1,3,5,7) - so a defect that collapses an image array to its base element, or
|
||||||
|
// that loses the units on the way to the composite, does not merely dim the result: it puts
|
||||||
|
// the wrong VALUE in a layer and names which one.
|
||||||
|
TEST_F(ImageLoadStoreSsoScenario, PerElementImageUnitsReachAPipelineDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
|
if (!PerElementImageUnitsAreHonoured()) {
|
||||||
|
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||||
|
}
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
constexpr int kWidth = 8;
|
||||||
|
constexpr int kHeight = 8;
|
||||||
|
constexpr int kLayers = 8;
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSsoVS);
|
||||||
|
const GLuint fs0 = MakeSeparable(GL_FRAGMENT_SHADER, kImageFS0);
|
||||||
|
const GLuint fs1 = MakeSeparable(GL_FRAGMENT_SHADER, kImageFS1);
|
||||||
|
if (vs == 0 || fs0 == 0 || fs1 == 0) return;
|
||||||
|
|
||||||
|
// Per ELEMENT, by name, on programs that are neither current nor attached to a bound
|
||||||
|
// pipeline yet - exactly the conformance call order.
|
||||||
|
const int units0[4] = {0, 2, 4, 6};
|
||||||
|
const int units1[4] = {1, 3, 5, 7};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
const std::string name = "g_image[" + std::to_string(i) + "]";
|
||||||
|
const GLint loc0 = glGetUniformLocation(fs0, name.c_str());
|
||||||
|
const GLint loc1 = glGetUniformLocation(fs1, name.c_str());
|
||||||
|
ASSERT_NE(loc0, -1) << "fs0 has no location for " << name;
|
||||||
|
ASSERT_NE(loc1, -1) << "fs1 has no location for " << name;
|
||||||
|
glProgramUniform1i(fs0, loc0, units0[i]);
|
||||||
|
glProgramUniform1i(fs1, loc1, units1[i]);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "assigning image units with glProgramUniform1i errored";
|
||||||
|
|
||||||
|
const GLuint pipeline0 = MakePipeline();
|
||||||
|
const GLuint pipeline1 = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline0, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline0, GL_FRAGMENT_SHADER_BIT, fs0);
|
||||||
|
glUseProgramStages(pipeline1, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline1, GL_FRAGMENT_SHADER_BIT, fs1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "pipeline setup errored";
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
const std::vector<float> zeros(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, 0.0f);
|
||||||
|
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA32F, kWidth, kHeight, kLayers, 0, GL_RGBA, GL_FLOAT, zeros.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA32F array texture errored";
|
||||||
|
|
||||||
|
// One LAYER of the array texture per unit, which is what makes each element's unit
|
||||||
|
// independently observable in the readback.
|
||||||
|
for (int unit = 0; unit < kLayers; ++unit) {
|
||||||
|
glBindImageTexture(static_cast<GLuint>(unit), texture, 0, GL_FALSE, unit, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
glBindProgramPipeline(pipeline0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glBindProgramPipeline(pipeline1);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the two pipeline draws leaked a GL error";
|
||||||
|
|
||||||
|
std::vector<float> readback(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, -1.0f);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, GL_FLOAT, readback.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the array texture back errored";
|
||||||
|
|
||||||
|
// Even layers were written through fs0's units, odd layers through fs1's.
|
||||||
|
for (int layer = 0; layer < kLayers; ++layer) {
|
||||||
|
const float expected = (layer % 2) ? 2.0f : 1.0f;
|
||||||
|
int offenders = 0;
|
||||||
|
float firstSeen = 0.0f;
|
||||||
|
for (int y = 0; y < kHeight; ++y) {
|
||||||
|
for (int x = 0; x < kWidth; ++x) {
|
||||||
|
const size_t base =
|
||||||
|
(static_cast<size_t>(layer) * kHeight * kWidth + static_cast<size_t>(y) * kWidth + x) * 4;
|
||||||
|
for (int c = 0; c < 4; ++c) {
|
||||||
|
if (readback[base + c] != expected) {
|
||||||
|
if (offenders == 0) firstSeen = readback[base + c];
|
||||||
|
++offenders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(offenders, 0) << "layer " << layer << " (image unit " << layer << ") expected " << expected
|
||||||
|
<< " but " << offenders << " components differ; first was " << firstSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// An image ARRAY sharing a program with another descriptor, which is the shape that makes
|
||||||
|
// the SPIR-V binding remap load-bearing.
|
||||||
|
//
|
||||||
|
// The remap (ProgramFactory::RemapDescriptorBindingsForVulkan) is what unifies bindings
|
||||||
|
// across stages and normalises every descriptor onto set 0; glslang hands it per-stage
|
||||||
|
// numbering that starts at 0 in EACH stage. It used to refuse any descriptor array that was
|
||||||
|
// not a UBO, and its only complaint was an assert that compiles out above DEBUG - so a
|
||||||
|
// release build carried on with the un-remapped numbering and a program holding an image
|
||||||
|
// array plus a second descriptor could see the two alias onto one binding, while a DEBUG
|
||||||
|
// build trapped on the very same program.
|
||||||
|
//
|
||||||
|
// A case with ONE descriptor cannot see any of that: with a single resource there is nothing
|
||||||
|
// to collide with and skipping the remap is indistinguishable from running it. Hence this
|
||||||
|
// one - an image array AND a uniform block in the same fragment program, with the block
|
||||||
|
// supplying the value that gets stored, so a mis-assigned binding shows up as the wrong
|
||||||
|
// colour rather than as nothing at all.
|
||||||
|
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
|
if (!PerElementImageUnitsAreHonoured()) {
|
||||||
|
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||||
|
}
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
constexpr int kWidth = 8;
|
||||||
|
constexpr int kHeight = 8;
|
||||||
|
constexpr int kLayers = 2;
|
||||||
|
|
||||||
|
static const char* kMixedFS = R"(#version 420 core
|
||||||
|
layout(rgba32f) uniform image2D g_image[2];
|
||||||
|
layout(std140) uniform Value { vec4 u_value; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < g_image.length(); ++i) {
|
||||||
|
imageStore(g_image[i], ivec2(gl_FragCoord), u_value);
|
||||||
|
}
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSsoVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kMixedFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
// Consecutive units here on purpose: this case is about the two descriptor KINDS
|
||||||
|
// coexisting, not about non-consecutive assignment, which the case above covers.
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
const std::string name = "g_image[" + std::to_string(i) + "]";
|
||||||
|
const GLint loc = glGetUniformLocation(fs, name.c_str());
|
||||||
|
ASSERT_NE(loc, -1) << "no location for " << name;
|
||||||
|
glProgramUniform1i(fs, loc, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLfloat value[4] = {7.0f, 7.0f, 7.0f, 7.0f};
|
||||||
|
GLuint ubo = 0;
|
||||||
|
glGenBuffers(1, &ubo);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, sizeof(value), value, GL_STATIC_DRAW);
|
||||||
|
const GLuint blockIndex = glGetUniformBlockIndex(fs, "Value");
|
||||||
|
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||||
|
glUniformBlockBinding(fs, blockIndex, 0);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "uniform block setup errored";
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
const std::vector<float> zeros(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, 0.0f);
|
||||||
|
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA32F, kWidth, kHeight, kLayers, 0, GL_RGBA, GL_FLOAT, zeros.data());
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
glBindImageTexture(1, texture, 0, GL_FALSE, 1, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "image texture setup errored";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the mixed-descriptor pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
std::vector<float> readback(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, -1.0f);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, GL_FLOAT, readback.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the array texture back errored";
|
||||||
|
|
||||||
|
for (int layer = 0; layer < kLayers; ++layer) {
|
||||||
|
int offenders = 0;
|
||||||
|
float firstSeen = 0.0f;
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(kWidth) * kHeight * 4; ++i) {
|
||||||
|
const size_t index = static_cast<size_t>(layer) * kHeight * kWidth * 4 + i;
|
||||||
|
if (readback[index] != 7.0f) {
|
||||||
|
if (offenders == 0) firstSeen = readback[index];
|
||||||
|
++offenders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(offenders, 0) << "layer " << layer << ": " << offenders
|
||||||
|
<< " components are not the uniform block's value; first was " << firstSeen
|
||||||
|
<< " (an image-array binding and a uniform block did not both survive)";
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &ubo);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same units, reassigned BETWEEN draws through the same pipeline. This is the half that
|
||||||
|
// the composite cache key change put weight on: the composite object now survives a
|
||||||
|
// glProgramUniform1i, so nothing rebuilds by accident and the new unit has to be carried by
|
||||||
|
// the refresh path (and, on Espryt, by regenerating the program the unit is baked into).
|
||||||
|
TEST_F(ImageLoadStoreSsoScenario, ReassigningAnImageUnitBetweenDrawsReachesTheNextDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
constexpr int kWidth = 8;
|
||||||
|
constexpr int kHeight = 8;
|
||||||
|
constexpr int kLayers = 2;
|
||||||
|
|
||||||
|
static const char* kSingleImageFS = R"(#version 420 core
|
||||||
|
layout(rgba32f) uniform image2D g_image;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(g_image, ivec2(gl_FragCoord), vec4(3.0));
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSsoVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSingleImageFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
const std::vector<float> zeros(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, 0.0f);
|
||||||
|
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA32F, kWidth, kHeight, kLayers, 0, GL_RGBA, GL_FLOAT, zeros.data());
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
glBindImageTexture(1, texture, 0, GL_FALSE, 1, GL_READ_WRITE, GL_RGBA32F);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "image texture setup errored";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
|
||||||
|
const GLint location = glGetUniformLocation(fs, "g_image");
|
||||||
|
ASSERT_NE(location, -1);
|
||||||
|
|
||||||
|
// Draw one against unit 0 (layer 0)...
|
||||||
|
glProgramUniform1i(fs, location, 0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
// ...and draw two against unit 1 (layer 1), with the composite already built and cached.
|
||||||
|
glProgramUniform1i(fs, location, 1);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the two pipeline draws leaked a GL error";
|
||||||
|
|
||||||
|
std::vector<float> readback(static_cast<size_t>(kWidth) * kHeight * kLayers * 4, -1.0f);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, GL_FLOAT, readback.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the array texture back errored";
|
||||||
|
|
||||||
|
for (int layer = 0; layer < kLayers; ++layer) {
|
||||||
|
int offenders = 0;
|
||||||
|
float firstSeen = 0.0f;
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(kWidth) * kHeight * 4; ++i) {
|
||||||
|
const size_t index = static_cast<size_t>(layer) * kHeight * kWidth * 4 + i;
|
||||||
|
if (readback[index] != 3.0f) {
|
||||||
|
if (offenders == 0) firstSeen = readback[index];
|
||||||
|
++offenders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(offenders, 0) << "layer " << layer << " was not written; " << offenders
|
||||||
|
<< " components differ, first was " << firstSeen
|
||||||
|
<< " (the image unit reassignment did not reach the draw)";
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.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 - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
|
||||||
|
//
|
||||||
|
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
|
||||||
|
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
|
||||||
|
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
|
||||||
|
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
|
||||||
|
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
|
||||||
|
// That is what it did here, on both backends, for two waves.
|
||||||
|
//
|
||||||
|
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
|
||||||
|
// ONE texture and checks the value that comes back, so a failure names the target kind and the
|
||||||
|
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
|
||||||
|
// bottom still does - a defect that only appears when several kinds share a program is invisible
|
||||||
|
// to the single-kind cases by construction.
|
||||||
|
//
|
||||||
|
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
|
||||||
|
//
|
||||||
|
// * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
|
||||||
|
// * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
|
||||||
|
// binding, with the image unit then assigned by glUniform1i. That combination is the one ES
|
||||||
|
// cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
|
||||||
|
// has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
|
||||||
|
// * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
|
||||||
|
// frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
|
||||||
|
// for a reason the conformance case excludes;
|
||||||
|
// * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
|
||||||
|
// to pass, and which is where a target kind whose layeredness a backend does not recognise
|
||||||
|
// goes wrong.
|
||||||
|
//
|
||||||
|
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
|
||||||
|
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
|
||||||
|
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
|
||||||
|
// conformance case never asks it for a multisample image at all. The two cases below are kept
|
||||||
|
// and skip on that same query, so the coverage is already written the day a backend advertises
|
||||||
|
// them - and so the skip is a standing record of WHY the conformance case passes without them.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The conformance case's own dimensions: one level, 6 on every axis (which is also
|
||||||
|
// exactly one cube's worth for a cube array), and a single texel read at the origin.
|
||||||
|
constexpr int kExtent = 6;
|
||||||
|
constexpr GLuint kFilledValue = 7u;
|
||||||
|
constexpr GLuint kStoredValue = 13u;
|
||||||
|
|
||||||
|
// Everything that differs between the eleven kinds, in one row.
|
||||||
|
struct TargetKind {
|
||||||
|
const char* name; // this scenario's name for it, which failure messages carry
|
||||||
|
GLenum target; // the GL texture target
|
||||||
|
const char* imageType; // the GLSL image uniform type
|
||||||
|
const char* coord; // the coordinate expression imageLoad/imageStore takes
|
||||||
|
bool multisample; // needs GL_MAX_IMAGE_SAMPLES > 0
|
||||||
|
bool buffer; // storage comes from a buffer object, not TexStorage
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
|
||||||
|
constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
|
||||||
|
false};
|
||||||
|
constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
|
||||||
|
constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
|
||||||
|
false};
|
||||||
|
constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
|
||||||
|
constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
|
||||||
|
constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
|
||||||
|
constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
|
||||||
|
"ivec3(0, 0, 0)", false, false};
|
||||||
|
constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
|
||||||
|
constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
|
||||||
|
constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
|
||||||
|
"ivec3(0, 0, 0)", true, false};
|
||||||
|
|
||||||
|
// A multisample image load/store takes the sample index as an extra argument; no other
|
||||||
|
// kind does. Keeping that in one place stops the two spellings drifting apart.
|
||||||
|
std::string LoadExpression(const TargetKind& kind, const std::string& name) {
|
||||||
|
return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
|
||||||
|
return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
|
||||||
|
value + ", 0, 0, 0));";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* kComputePrologue = "#version 440 core\n"
|
||||||
|
"\n"
|
||||||
|
"layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
|
||||||
|
"\n";
|
||||||
|
|
||||||
|
const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
|
||||||
|
" uint sum;\n"
|
||||||
|
"} ssb;\n"
|
||||||
|
"\n";
|
||||||
|
|
||||||
|
// The conformance case's shader, narrowed to a single image.
|
||||||
|
std::string SingleLoadSource(const TargetKind& kind) {
|
||||||
|
return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
|
||||||
|
" i0;\n" + kResultBlock + "void main()\n{\n uvec4 v = " + LoadExpression(kind, "i0") +
|
||||||
|
";\n ssb.sum = v.r;\n}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other direction. Written as its own program rather than a read-write one so that a
|
||||||
|
// backend which gets the store right and the load wrong (or the reverse) is not able to
|
||||||
|
// cancel its own defect out.
|
||||||
|
std::string SingleStoreSource(const TargetKind& kind) {
|
||||||
|
return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
|
||||||
|
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageTargetKindScenario : 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);
|
||||||
|
for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
|
||||||
|
m_programs.clear();
|
||||||
|
m_textures.clear();
|
||||||
|
m_buffers.clear();
|
||||||
|
// Leave no image unit bound. These scenarios share one context, and a stale image
|
||||||
|
// binding is exactly the kind of state that makes the NEXT scenario's failure
|
||||||
|
// impossible to reproduce on its own.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||||
|
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 >= 1 && maxComputeImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The conformance case's own multisample gate, asked the same way it asks it.
|
||||||
|
bool MultisampleImagesAreUsable() const {
|
||||||
|
GLint maxImageSamples = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageSamples > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 << "\nsource:\n" << source;
|
||||||
|
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 << "\nsource:\n" << source;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage plus a full fill with `value`, in the spelling each target kind needs.
|
||||||
|
// Returns 0 - having already reported - when the target could not be created.
|
||||||
|
GLuint MakeTexture(const TargetKind& kind, bool fill, GLuint value = kFilledValue) {
|
||||||
|
const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, value);
|
||||||
|
|
||||||
|
if (kind.buffer) {
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
m_buffers.push_back(buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
|
||||||
|
fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
|
||||||
|
<< GLErrorName(error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(kind.target, texture);
|
||||||
|
|
||||||
|
switch (kind.target) {
|
||||||
|
case GL_TEXTURE_1D:
|
||||||
|
glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D:
|
||||||
|
case GL_TEXTURE_RECTANGLE:
|
||||||
|
case GL_TEXTURE_1D_ARRAY:
|
||||||
|
case GL_TEXTURE_CUBE_MAP:
|
||||||
|
glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D_ARRAY:
|
||||||
|
case GL_TEXTURE_3D:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||||
|
glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||||
|
glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||||
|
glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A multisample texture has no TexSubImage - the conformance case fills it with a
|
||||||
|
// compute pass, which is what the store cases below do.
|
||||||
|
if (!fill || kind.multisample) return texture;
|
||||||
|
|
||||||
|
switch (kind.target) {
|
||||||
|
case GL_TEXTURE_1D:
|
||||||
|
glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D:
|
||||||
|
case GL_TEXTURE_RECTANGLE:
|
||||||
|
case GL_TEXTURE_1D_ARRAY:
|
||||||
|
glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||||
|
texels.data());
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_CUBE_MAP:
|
||||||
|
for (int face = 0; face < 6; ++face) {
|
||||||
|
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||||
|
kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_2D_ARRAY:
|
||||||
|
case GL_TEXTURE_3D:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||||
|
glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
|
||||||
|
GL_UNSIGNED_INT, texels.data());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
|
||||||
|
GLuint MakeResultBuffer() {
|
||||||
|
GLuint ssbo = 0;
|
||||||
|
glGenBuffers(1, &ssbo);
|
||||||
|
m_buffers.push_back(ssbo);
|
||||||
|
const GLuint zero = 0u;
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
|
||||||
|
return ssbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint ReadResult(GLuint ssbo) {
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||||
|
GLuint value = 0xFFFFFFFFu;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
|
||||||
|
// compute dispatch, and require the value back.
|
||||||
|
void RunLoadCase(const TargetKind& kind) {
|
||||||
|
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
|
||||||
|
if (program == 0) return;
|
||||||
|
const GLuint texture = MakeTexture(kind, true);
|
||||||
|
if (texture == 0) return;
|
||||||
|
const GLuint ssbo = MakeResultBuffer();
|
||||||
|
|
||||||
|
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
// The unit, by LOCATION - the conformance case's own redundant-but-legal
|
||||||
|
// assignment, and the one ES cannot take at the API level.
|
||||||
|
glUniform1i(0, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
|
||||||
|
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";
|
||||||
|
|
||||||
|
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
|
||||||
|
<< kind.name << ": the compute dispatch did not read the value the texture was filled with";
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other direction: store through an image uniform, then read the same texel back
|
||||||
|
// through a SECOND program, so a defect cannot cancel itself out.
|
||||||
|
void RunStoreCase(const TargetKind& kind) {
|
||||||
|
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
|
||||||
|
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
|
||||||
|
if (storeProgram == 0 || loadProgram == 0) return;
|
||||||
|
const GLuint texture = MakeTexture(kind, false);
|
||||||
|
if (texture == 0) return;
|
||||||
|
const GLuint ssbo = MakeResultBuffer();
|
||||||
|
|
||||||
|
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||||
|
|
||||||
|
glUseProgram(storeProgram);
|
||||||
|
glUniform1i(0, 0);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";
|
||||||
|
|
||||||
|
glUseProgram(loadProgram);
|
||||||
|
glUniform1i(0, 0);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
|
||||||
|
|
||||||
|
EXPECT_EQ(ReadResult(ssbo), kStoredValue)
|
||||||
|
<< kind.name << ": the value stored through the image did not come back";
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
std::vector<GLuint> m_buffers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---- the load direction, one target kind per case -----------------------
|
||||||
|
//
|
||||||
|
// Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
|
||||||
|
// failure names the kind.
|
||||||
|
|
||||||
|
#define MGL_DEFINE_LOAD_CASE(CaseName, Kind) \
|
||||||
|
TEST_F(ImageTargetKindScenario, Loads##CaseName) { \
|
||||||
|
if (!Ready()) return; \
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||||
|
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||||
|
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||||
|
"here and never asks for a multisample one"; \
|
||||||
|
} \
|
||||||
|
RunLoadCase(Kind); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MGL_DEFINE_STORE_CASE(CaseName, Kind) \
|
||||||
|
TEST_F(ImageTargetKindScenario, Stores##CaseName) { \
|
||||||
|
if (!Ready()) return; \
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||||
|
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||||
|
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||||
|
"here and never asks for a multisample one"; \
|
||||||
|
} \
|
||||||
|
RunStoreCase(Kind); \
|
||||||
|
}
|
||||||
|
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
|
||||||
|
MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
|
||||||
|
MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
|
||||||
|
MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
|
||||||
|
MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
|
||||||
|
MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||||
|
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
|
||||||
|
MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
|
||||||
|
MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
|
||||||
|
MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
|
||||||
|
MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
|
||||||
|
MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||||
|
|
||||||
|
#undef MGL_DEFINE_LOAD_CASE
|
||||||
|
#undef MGL_DEFINE_STORE_CASE
|
||||||
|
|
||||||
|
// ---- and all of them at once -------------------------------------------
|
||||||
|
//
|
||||||
|
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
|
||||||
|
// needs several kinds in one program - a binding remap that only collides when two image
|
||||||
|
// types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
|
||||||
|
// - and that class of defect is precisely what "each kind passes alone but the case still
|
||||||
|
// fails" would mean.
|
||||||
|
//
|
||||||
|
// Each unit is filled with its own DISTINCT value rather than a shared one, so a shortfall
|
||||||
|
// names WHICH kind is missing rather than merely how many are: with one shared value, "three
|
||||||
|
// kinds read zero" and "one kind read zero" differ only by a multiple, and any two kinds are
|
||||||
|
// interchangeable in the total. A sum still cannot see two kinds SWAPPING - addition is
|
||||||
|
// commutative, and the conformance case has exactly the same blind spot - but the single-kind
|
||||||
|
// cases above pin each kind to its own texture already, so a swap cannot hide there.
|
||||||
|
TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
||||||
|
|
||||||
|
// The two kinds this whole scenario file exists for come FIRST, and that ordering is
|
||||||
|
// load-bearing rather than cosmetic. The list has to be truncated to the device's image
|
||||||
|
// unit count, and the guaranteed minimum is small - ES 3.1 promises only four compute
|
||||||
|
// image uniforms - so a list in the conformance case's own order would put imageBuffer
|
||||||
|
// at index five and drop it on exactly the devices most likely to get it wrong. A test
|
||||||
|
// that quietly stops covering its own subject is worse than one that fails.
|
||||||
|
const bool multisample = MultisampleImagesAreUsable();
|
||||||
|
std::vector<TargetKind> kinds{kKind1DArray, kKindBuffer, kKind2D, kKind1D, kKind2DArray,
|
||||||
|
kKind3D, kKindCube, kKindRect, kKindCubeArray};
|
||||||
|
if (multisample) {
|
||||||
|
kinds.push_back(kKind2DMS);
|
||||||
|
kinds.push_back(kKind2DMSArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
const std::size_t count =
|
||||||
|
std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
|
||||||
|
maxImageUnits))));
|
||||||
|
if (count == 0) GTEST_SKIP() << "no image units";
|
||||||
|
// Named, not silently dropped: `expected` is computed over whatever survives, so a
|
||||||
|
// truncated run is self-consistently green and would otherwise never say what it stopped
|
||||||
|
// covering.
|
||||||
|
if (count < kinds.size()) {
|
||||||
|
std::string dropped;
|
||||||
|
for (std::size_t i = count; i < kinds.size(); ++i) {
|
||||||
|
if (!dropped.empty()) dropped += ", ";
|
||||||
|
dropped += kinds[i].name;
|
||||||
|
}
|
||||||
|
RecordProperty("dropped_image_target_kinds", dropped);
|
||||||
|
GTEST_LOG_(INFO) << "only " << count << " image units, so these kinds are not covered by the "
|
||||||
|
<< "combined case: " << dropped;
|
||||||
|
}
|
||||||
|
kinds.resize(count);
|
||||||
|
|
||||||
|
std::string declarations;
|
||||||
|
std::string sum;
|
||||||
|
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||||
|
const std::string name = "i" + std::to_string(i);
|
||||||
|
declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
|
||||||
|
kinds[i].imageType + " " + name + ";\n";
|
||||||
|
if (!sum.empty()) sum += " + ";
|
||||||
|
sum += LoadExpression(kinds[i], name);
|
||||||
|
}
|
||||||
|
const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
|
||||||
|
"void main()\n{\n uvec4 v = " + sum + ";\n ssb.sum = v.r;\n}\n";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(source);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
// Powers of two, so the shortfall's bit pattern names exactly which kinds read zero -
|
||||||
|
// no other subset of the values can sum to the same total. Eleven kinds at most, so the
|
||||||
|
// largest is 1 << 10 and the sum cannot approach a uint's range.
|
||||||
|
GLuint expected = 0;
|
||||||
|
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||||
|
const GLuint value = 1u << i;
|
||||||
|
const GLuint texture = MakeTexture(kinds[i], true, value);
|
||||||
|
if (texture == 0) return;
|
||||||
|
expected += value;
|
||||||
|
glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
|
||||||
|
}
|
||||||
|
const GLuint ssbo = MakeResultBuffer();
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||||
|
glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";
|
||||||
|
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||||
|
|
||||||
|
const GLuint actual = ReadResult(ssbo);
|
||||||
|
std::string missing;
|
||||||
|
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||||
|
if ((actual & (1u << i)) == 0u) {
|
||||||
|
if (!missing.empty()) missing += ", ";
|
||||||
|
missing += kinds[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(actual, expected)
|
||||||
|
<< "the sum over " << kinds.size()
|
||||||
|
<< " image target kinds is wrong; each kind contributes its own bit, and these read "
|
||||||
|
"zero: "
|
||||||
|
<< (missing.empty() ? "(none - so some kind read a value it was never given)" : missing);
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentBarrierScenario.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 TRANSFER OFF A NON-ZERO ATTACHMENT LAYER READS THE LAYER THE BARRIER MOVED.
|
||||||
|
//
|
||||||
|
// Every transfer DirectVulkan performs against a framebuffer attachment is three commands: a
|
||||||
|
// barrier that puts the image in TRANSFER_SRC/DST, the copy or blit itself, and a barrier that
|
||||||
|
// puts it back. The copy names the attachment's layer - glFramebufferTextureLayer(.., layer) ends
|
||||||
|
// up in `srcSubresource.baseArrayLayer` - but TransitionImageLayout used to emit `layerCount = 1`
|
||||||
|
// from `baseArrayLayer 0`, so for every attachment on a layer above zero the barrier moved layer 0
|
||||||
|
// and the copy read layer N. The layer the transfer touched was never transitioned: it sat in
|
||||||
|
// COLOR_ATTACHMENT_OPTIMAL (or DEPTH_STENCIL_ATTACHMENT_OPTIMAL) while being read as TRANSFER_SRC.
|
||||||
|
//
|
||||||
|
// That is undefined behaviour, not a guaranteed wrong pixel: a layout is a compression/tiling
|
||||||
|
// promise, so a driver that stores both layouts identically returns the right bytes anyway. The
|
||||||
|
// software lanes (lavapipe) are exactly such a driver, which is why this scenario is paired with a
|
||||||
|
// validation-layer run - the layer names the mismatch outright
|
||||||
|
// (VUID-vkCmdCopyImageToBuffer-srcImageLayout-00189, "srcImageLayout ... doesn't match the actual
|
||||||
|
// current layout") where the pixels here cannot. On a tiler that really does re-tile per layout,
|
||||||
|
// these are the reads that come back as garbage.
|
||||||
|
//
|
||||||
|
// The four cases below are the four transfer paths that take an attachment layer from GL:
|
||||||
|
//
|
||||||
|
// glReadPixels (colour) -> VulkanRenderer::ReadPixels
|
||||||
|
// glBlitFramebuffer (colour) -> VulkanRenderer::BlitNamedFramebuffer
|
||||||
|
// glReadPixels (GL_DEPTH_COMPONENT) -> VulkanRenderer::ReadDepthStencilImageToClient
|
||||||
|
// glBlitFramebuffer (GL_DEPTH_BUFFER_BIT) -> VulkanRenderer::BlitNamedFramebuffer, depth leg
|
||||||
|
//
|
||||||
|
// Each one renders or clears INTO the non-zero layer first, so the image is genuinely sitting in
|
||||||
|
// its attachment layout when the transfer starts - a scenario that only uploaded texels would
|
||||||
|
// leave it in a transfer layout already and the mismatched barrier would be a no-op.
|
||||||
|
//
|
||||||
|
// Every case also asserts the layers it did not name still hold their own fill, so a backend that
|
||||||
|
// "fixed" the miss by transferring the whole image passes neither half.
|
||||||
|
//
|
||||||
|
// DirectGLES is the control: it hands the same calls to the driver, so a failure on both backends
|
||||||
|
// means the scenario is wrong and a failure on DirectVulkan alone means Magma is.
|
||||||
|
|
||||||
|
#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 int kWidth = 8;
|
||||||
|
constexpr int kHeight = 8;
|
||||||
|
// Four layers with the subject at index 2: layers on both sides of it stay untouched, so
|
||||||
|
// "moved the whole image" and "moved layer 0" are both distinguishable from correct.
|
||||||
|
constexpr int kLayers = 4;
|
||||||
|
constexpr int kSubjectLayer = 2;
|
||||||
|
|
||||||
|
// A value no correct read can produce, so "the backend wrote nothing" fails loudly.
|
||||||
|
constexpr float kDepthPoison = 0.2f;
|
||||||
|
|
||||||
|
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-layer fill, uniform within a layer: the defect is about WHICH layer is addressed, and
|
||||||
|
// a value that also varied inside the layer would make the assertions depend on row order.
|
||||||
|
Rgba8 LayerFill(int layer) {
|
||||||
|
return {static_cast<GLubyte>(17 + layer * 30), static_cast<GLubyte>(200 - layer * 25),
|
||||||
|
static_cast<GLubyte>(60 + layer * 40), 255};
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the draw paints - matches kFS below, and is deliberately none of the LayerFill
|
||||||
|
// values so "the draw never landed" cannot read as a pass.
|
||||||
|
constexpr Rgba8 kPaintedColor{26, 51, 204, 255};
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.1, 0.2, 0.8, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LayeredAttachmentBarrierScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
for (const GLuint fbo : m_fbos) {
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
}
|
||||||
|
m_fbos.clear();
|
||||||
|
for (const GLuint texture : m_textures) {
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
m_textures.clear();
|
||||||
|
if (m_program != 0) {
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(m_program);
|
||||||
|
m_program = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RGBA8 2D array with a different uniform colour per layer.
|
||||||
|
GLuint MakeColorArray() {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
for (int layer = 0; layer < kLayers; ++layer) {
|
||||||
|
const std::vector<Rgba8> texels(static_cast<std::size_t>(kWidth) * kHeight, LayerFill(layer));
|
||||||
|
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kWidth, kHeight, 1, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
}
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A depth 2D array. No initial upload: depth arrays are filled by clearing through an
|
||||||
|
// attachment, which is also the state the transfer paths have to cope with.
|
||||||
|
GLuint MakeDepthArray() {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||||
|
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kWidth, kHeight, kLayers);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One FBO naming `layer` of the given arrays. Depth is optional (0 = colour only).
|
||||||
|
GLuint MakeLayerFbo(GLuint colorArray, GLuint depthArray, int layer) {
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
m_fbos.push_back(fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorArray, 0, layer);
|
||||||
|
if (depthArray != 0) {
|
||||||
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, layer);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "layer " << layer << " is not attachable";
|
||||||
|
return fbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glReadPixels of one whole layer, through an FBO that names it.
|
||||||
|
Rgba8 ReadLayer(GLuint colorArray, int layer) {
|
||||||
|
const GLuint fbo = MakeLayerFbo(colorArray, 0, layer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
// The fill is uniform within a layer, so any disagreement between texels is itself
|
||||||
|
// a failure - reported here rather than silently reduced to pixels[0].
|
||||||
|
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||||
|
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||||
|
<< "layer " << layer << " is not uniform: texel 0 is " << Describe(pixels[0]) << ", texel "
|
||||||
|
<< i << " is " << Describe(pixels[i]);
|
||||||
|
}
|
||||||
|
return pixels[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every layer but `changed` still holds its own fill.
|
||||||
|
void ExpectOtherLayersUntouched(GLuint colorArray, int changed, const char* what) {
|
||||||
|
for (int layer = 0; layer < kLayers; ++layer) {
|
||||||
|
if (layer == changed) continue;
|
||||||
|
const Rgba8 actual = ReadLayer(colorArray, layer);
|
||||||
|
EXPECT_TRUE(actual == LayerFill(layer))
|
||||||
|
<< what << ": layer " << layer << " should still hold its fill but is " << Describe(actual)
|
||||||
|
<< ", expected " << Describe(LayerFill(layer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float ReadDepthAt(int x, int y) const {
|
||||||
|
float depth = kDepthPoison;
|
||||||
|
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
std::vector<GLuint> m_fbos;
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// glReadPixels straight off a layer that was just rendered to. The image is in
|
||||||
|
// COLOR_ATTACHMENT_OPTIMAL when the readback barrier runs, so the barrier and the copy
|
||||||
|
// disagreeing about the layer is a live layout mismatch, not a bookkeeping detail.
|
||||||
|
TEST_F(LayeredAttachmentBarrierScenario, ReadPixelsOffRenderedNonZeroLayer) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint colorArray = MakeColorArray();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||||
|
|
||||||
|
const GLuint fbo = MakeLayerFbo(colorArray, 0, kSubjectLayer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
DrawFullViewportQuad(m_program);
|
||||||
|
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||||
|
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < pixels.size(); ++i) {
|
||||||
|
ASSERT_NEAR(pixels[i].r, kPaintedColor.r, 2)
|
||||||
|
<< "texel " << i << " of the rendered layer is " << Describe(pixels[i]);
|
||||||
|
ASSERT_NEAR(pixels[i].g, kPaintedColor.g, 2) << "texel " << i;
|
||||||
|
ASSERT_NEAR(pixels[i].b, kPaintedColor.b, 2) << "texel " << i;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectOtherLayersUntouched(colorArray, kSubjectLayer, "readback off a rendered layer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// glBlitFramebuffer between two non-zero layers of two different arrays. Both endpoints are
|
||||||
|
// above layer 0, so the source and destination barriers are each wrong on their own side.
|
||||||
|
TEST_F(LayeredAttachmentBarrierScenario, BlitBetweenNonZeroColorLayers) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint sourceArray = MakeColorArray();
|
||||||
|
const GLuint destinationArray = MakeColorArray();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSourceLayer = 3;
|
||||||
|
constexpr int kDestinationLayer = 1;
|
||||||
|
|
||||||
|
const GLuint sourceFbo = MakeLayerFbo(sourceArray, 0, kSourceLayer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
DrawFullViewportQuad(m_program);
|
||||||
|
|
||||||
|
const GLuint destinationFbo = MakeLayerFbo(destinationArray, 0, kDestinationLayer);
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||||
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const Rgba8 blitted = ReadLayer(destinationArray, kDestinationLayer);
|
||||||
|
EXPECT_NEAR(blitted.r, kPaintedColor.r, 2) << "blit destination layer is " << Describe(blitted);
|
||||||
|
EXPECT_NEAR(blitted.g, kPaintedColor.g, 2);
|
||||||
|
EXPECT_NEAR(blitted.b, kPaintedColor.b, 2);
|
||||||
|
|
||||||
|
ExpectOtherLayersUntouched(destinationArray, kDestinationLayer, "colour blit destination");
|
||||||
|
// The source layer was rendered, not blitted into, so it is checked separately.
|
||||||
|
const Rgba8 source = ReadLayer(sourceArray, kSourceLayer);
|
||||||
|
EXPECT_NEAR(source.r, kPaintedColor.r, 2) << "blit source layer is " << Describe(source);
|
||||||
|
ExpectOtherLayersUntouched(sourceArray, kSourceLayer, "colour blit source");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The depth aspect of the same readback path: the depth image sits in
|
||||||
|
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL after the clear, and the copy names the attached layer.
|
||||||
|
TEST_F(LayeredAttachmentBarrierScenario, ReadDepthOffClearedNonZeroLayer) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint colorArray = MakeColorArray();
|
||||||
|
const GLuint depthArray = MakeDepthArray();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||||
|
|
||||||
|
const GLuint fbo = MakeLayerFbo(colorArray, depthArray, kSubjectLayer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glClearDepth(0.375);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
const float centre = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(centre, 0.375f, 1.0f / 4096.0f)
|
||||||
|
<< "glReadPixels(GL_DEPTH_COMPONENT) off layer " << kSubjectLayer << " returned " << centre
|
||||||
|
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The depth leg of the blit path, both endpoints above layer 0. Verified by reading the
|
||||||
|
// destination's depth back, which is the same readback the case above pins - so a failure
|
||||||
|
// here with that one passing is the blit, not the readback.
|
||||||
|
TEST_F(LayeredAttachmentBarrierScenario, BlitDepthBetweenNonZeroLayers) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint sourceColor = MakeColorArray();
|
||||||
|
const GLuint sourceDepth = MakeDepthArray();
|
||||||
|
const GLuint destinationColor = MakeColorArray();
|
||||||
|
const GLuint destinationDepth = MakeDepthArray();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||||
|
|
||||||
|
constexpr int kSourceLayer = 3;
|
||||||
|
constexpr int kDestinationLayer = 1;
|
||||||
|
|
||||||
|
const GLuint sourceFbo = MakeLayerFbo(sourceColor, sourceDepth, kSourceLayer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glClearDepth(0.625);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
// A destination pre-cleared to something the blit must overwrite, so "the blit did
|
||||||
|
// nothing" and "the blit landed" are different answers.
|
||||||
|
const GLuint destinationFbo = MakeLayerFbo(destinationColor, destinationDepth, kDestinationLayer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||||
|
glViewport(0, 0, kWidth, kHeight);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glClearDepth(0.125);
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||||
|
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||||
|
const float blitted = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_NEAR(blitted, 0.625f, 1.0f / 4096.0f)
|
||||||
|
<< "depth blitted onto layer " << kDestinationLayer << " reads back as " << blitted
|
||||||
|
<< (std::fabs(blitted - 0.125f) < 1e-3f ? " - the destination kept its own clear" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -101,6 +102,39 @@ void main() {
|
|||||||
// "every single pixel" an achievable (and therefore useful) demand.
|
// "every single pixel" an achievable (and therefore useful) demand.
|
||||||
constexpr int kQuadrantInset = 2;
|
constexpr int kQuadrantInset = 2;
|
||||||
|
|
||||||
|
// A deliberately asymmetric sub-rect of the 128x96 surface: neither centred nor
|
||||||
|
// full-extent in either axis, mirroring the conformance suite's randomised
|
||||||
|
// sub-viewport geometry (glcShaderRenderCase.cpp:735-741). Asymmetry is the whole
|
||||||
|
// point - y == H - y - h is exactly the case an unconverted Y origin gets right by
|
||||||
|
// accident, and it is the only case the shipped code ever exercised.
|
||||||
|
// correct band = GL rows [13, 55)
|
||||||
|
// mirrored band = GL rows [41, 83) (what H-y-h produces)
|
||||||
|
constexpr int kSubX = 17;
|
||||||
|
constexpr int kSubY = 13;
|
||||||
|
constexpr int kSubW = 60;
|
||||||
|
constexpr int kSubH = 42;
|
||||||
|
|
||||||
|
Image CropRect(const Image& source, int x0, int y0, int width, int height) {
|
||||||
|
Image out(width, height);
|
||||||
|
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
const std::uint8_t* sourceRow =
|
||||||
|
source.Data() + (static_cast<std::size_t>(y0 + y) * source.Width() + x0) * 4;
|
||||||
|
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes, sourceRow, rowBytes);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image VFlip(const Image& source) {
|
||||||
|
Image out(source.Width(), source.Height());
|
||||||
|
const std::size_t rowBytes = static_cast<std::size_t>(source.Width()) * 4;
|
||||||
|
for (int y = 0; y < source.Height(); ++y) {
|
||||||
|
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes,
|
||||||
|
source.Data() + static_cast<std::size_t>(source.Height() - 1 - y) * rowBytes, rowBytes);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
float x, y;
|
float x, y;
|
||||||
float r, g, b;
|
float r, g, b;
|
||||||
@@ -377,5 +411,174 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ sub-rect / M-1 ----
|
||||||
|
//
|
||||||
|
// Everything above reads the FULL extent of its target, which is the one case
|
||||||
|
// DirectVulkan's default-framebuffer readback ever re-oriented: the remap at
|
||||||
|
// VulkanRenderer.cpp:2042 had no rect parameters at all, so :8278 gated it on
|
||||||
|
// `width == swapchainExtent.width && height == swapchainExtent.height` and fell back to a
|
||||||
|
// raw copy otherwise. Meanwhile the viewport (:422), the scissor (:506-546) and the
|
||||||
|
// ReadPixels copy offset (:8238) all used the GL bottom-origin Y verbatim as a Vulkan
|
||||||
|
// top-origin Y.
|
||||||
|
//
|
||||||
|
// In the conformance suite those defects CANCEL in placement - the draw lands in Vulkan
|
||||||
|
// rows [y, y+h) and the readback copies the same rows back - and compose into an exact
|
||||||
|
// vertical flip of a correct image. That is 1,759 of Magma's 1,793 non-pass cases, and
|
||||||
|
// image forensics over all 861 gl33 failures found 861 vertical flips and nothing else.
|
||||||
|
// Taken apart, they are two independent user-visible bugs, so they are tested apart:
|
||||||
|
// SubViewportDraw pins placement with a full-extent read, SubRectReadback pins the
|
||||||
|
// readback rect after a full-viewport draw, and SubViewportSubRectRoundTrip is the CTS
|
||||||
|
// shape where the two cancel.
|
||||||
|
|
||||||
|
// Placement: a sub-viewport draw must land in GL rows [y0, y0+h), not mirrored about the
|
||||||
|
// surface centre. Read back full-extent, which is the path that already worked, so a
|
||||||
|
// failure here can only be the viewport's Y origin.
|
||||||
|
TEST_F(OrientationScenario, SubViewportDrawLandsWhereGLPutsIt) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
|
||||||
|
const Image placed = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(placed.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the sub-viewport draw is not upright inside its own rect";
|
||||||
|
ExpectUprightQuadrants(placed, "sub-viewport draw, cropped out of a full-extent read");
|
||||||
|
|
||||||
|
// Nothing may have been painted outside the viewport. This is what catches the
|
||||||
|
// mirrored placement: the drawn band would sit at GL rows [41, 83) instead.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, 0, kSubY - 2, "black", 0.0,
|
||||||
|
"below the sub-viewport"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, kSubY + kSubH + 1, Gl().Height() - 1, "black",
|
||||||
|
0.0, "above the sub-viewport"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readback: a full-viewport draw read back through a sub-rect must return the requested
|
||||||
|
// band, in GL row order. Band and orientation are asserted separately so that fixing only
|
||||||
|
// one of the two cannot pass this case.
|
||||||
|
TEST_F(OrientationScenario, SubRectReadbackReturnsTheRequestedBandUpright) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
||||||
|
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ASSERT_EQ(sub.Width(), kSubW);
|
||||||
|
ASSERT_EQ(sub.Height(), kSubH);
|
||||||
|
|
||||||
|
const Image requestedBand = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
const Image mirroredBand = CropRect(whole, kSubX, Gl().Height() - kSubY - kSubH, kSubW, kSubH);
|
||||||
|
|
||||||
|
// The geometry has to be able to see both mistakes; if a future surface size made the
|
||||||
|
// band symmetric these assertions would be vacuous, so say so loudly instead.
|
||||||
|
ASSERT_FALSE(requestedBand == VFlip(requestedBand))
|
||||||
|
<< "the chosen sub-rect is vertically symmetric - it cannot detect a row flip";
|
||||||
|
ASSERT_FALSE(requestedBand == mirroredBand)
|
||||||
|
<< "the chosen sub-rect equals its mirror band - it cannot detect a wrong band";
|
||||||
|
|
||||||
|
EXPECT_FALSE(sub == VFlip(requestedBand))
|
||||||
|
<< "ORIENTATION: the requested band came back with its rows in Vulkan (top-first) order";
|
||||||
|
EXPECT_FALSE(sub == mirroredBand || sub == VFlip(mirroredBand))
|
||||||
|
<< "BAND: the read returned GL rows [H-y-h, H-y) instead of [y, y+h)";
|
||||||
|
EXPECT_TRUE(sub == requestedBand)
|
||||||
|
<< "the sub-rect readback differs from the same rect of the full-extent read in "
|
||||||
|
<< sub.ByteDiffCount(requestedBand) << " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exact conformance-suite shape: an asymmetric sub-viewport draw read back through the
|
||||||
|
// very same sub-rect. The placement and readback errors cancel, leaving an image that is
|
||||||
|
// correct in every pixel VALUE and vertically flipped - which is precisely the 861-case
|
||||||
|
// signature. One assertion, and it pins all of them.
|
||||||
|
TEST_F(OrientationScenario, SubViewportSubRectRoundTripIsUpright) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(sub.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "sub-viewport draw + same-rect readback came back flipped - this is the shape "
|
||||||
|
"behind KHR-GL33/GL40.shaders.* (861 cases each)";
|
||||||
|
ExpectUprightQuadrants(sub, "sub-viewport draw read back through the same sub-rect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same conversion, on the other rect consumer that reads the default framebuffer.
|
||||||
|
// glBlitFramebuffer already converted its DESTINATION rect when the draw framebuffer was
|
||||||
|
// the default one (ApplyNativeBlitDefaultFramebufferTransform), but never its SOURCE rect,
|
||||||
|
// so a blit OUT of the default framebuffer took the mirrored band and wrote it upside
|
||||||
|
// down. Blitting a sub-rect and comparing against the same sub-rect of a direct read pins
|
||||||
|
// both halves at once.
|
||||||
|
TEST_F(OrientationScenario, BlitOutOfTheDefaultFramebufferKeepsBandAndOrientation) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
||||||
|
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_offscreen.fbo);
|
||||||
|
glBlitFramebuffer(kSubX, kSubY, kSubX + kSubW, kSubY + kSubH, kSubX, kSubY, kSubX + kSubW,
|
||||||
|
kSubY + kSubH, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||||
|
const unsigned int blitError = FirstGLError();
|
||||||
|
if (blitError != GL_NO_ERROR) {
|
||||||
|
GTEST_SKIP() << "this backend refused the default-framebuffer blit: "
|
||||||
|
<< GLErrorName(blitError);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_offscreen.fbo);
|
||||||
|
const Image blitted = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
|
||||||
|
const Image landed = CropRect(blitted, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
const Image expected = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_FALSE(landed == VFlip(expected))
|
||||||
|
<< "ORIENTATION: the blitted band arrived upside down";
|
||||||
|
EXPECT_TRUE(landed == expected)
|
||||||
|
<< "the blitted sub-rect differs from the same sub-rect of a direct read in "
|
||||||
|
<< landed.ByteDiffCount(expected) << " bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control. A non-default framebuffer is already self-consistent - no
|
||||||
|
// gl_Position.y negation, GL row 0 IS Vulkan row 0 - so none of the fixes above may touch
|
||||||
|
// it. If this ever starts failing, the default-FBO remap has leaked into the FBO path.
|
||||||
|
TEST_F(OrientationScenario, FboSubRectReadbackAndSubViewportAreUnaffected) {
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
|
||||||
|
const Image whole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(sub == CropRect(whole, kSubX, kSubY, kSubW, kSubH))
|
||||||
|
<< "an FBO sub-rect readback differs from the same rect of its full-extent read in "
|
||||||
|
<< sub.ByteDiffCount(CropRect(whole, kSubX, kSubY, kSubW, kSubH)) << " bytes";
|
||||||
|
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glViewport(kSubX, kSubY, kSubW, kSubH);
|
||||||
|
DrawQuadrants();
|
||||||
|
glViewport(0, 0, m_offscreen.width, m_offscreen.height);
|
||||||
|
const Image placedWhole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(CropRect(placedWhole, kSubX, kSubY, kSubW, kSubH).QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "an FBO sub-viewport draw must land in GL rows [y0, y0+h) upright";
|
||||||
|
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, 0, kSubY - 2, "black", 0.0,
|
||||||
|
"below an FBO sub-viewport"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, kSubY + kSubH + 1,
|
||||||
|
m_offscreen.height - 1, "black", 0.0, "above an FBO sub-viewport"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipelineFailureScenario.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 draw had no pipeline, so we bound null."
|
||||||
|
//
|
||||||
|
// DirectVulkan's SetupDraw called GetOrCreatePipeline - a function that DOCUMENTS a
|
||||||
|
// VK_NULL_HANDLE return - and passed the result straight to vkCmdBindPipeline. When the
|
||||||
|
// Adreno driver answered vkCreateGraphicsPipelines with VK_ERROR_UNKNOWN, the next
|
||||||
|
// instruction dereferenced null inside the driver: SIGSEGV at fault addr 0x8, and that one
|
||||||
|
// shape accounted for 9 of the 15 process deaths in the 2026-08-10 GL-CTS run
|
||||||
|
// (KHR-GL33/GL40.shaders.struct.uniform.sampler_array_vertex, six
|
||||||
|
// KHR-GL42.shader_image_load_store cases, one shader_storage_buffer_object case).
|
||||||
|
//
|
||||||
|
// It was made permanent by a second defect: PipelineFactory memoized the failure, so the
|
||||||
|
// null was served for the rest of the process. Every later draw with the same state died
|
||||||
|
// too, which is why a single bad program took whole CTS groups down with it.
|
||||||
|
//
|
||||||
|
// What this scenario pins, on both backends:
|
||||||
|
// 1. The GL program shape the CTS crashed on (an array of structs each containing a
|
||||||
|
// sampler, sampled from the VERTEX stage) draws without killing the process.
|
||||||
|
// 2. It draws AGAIN and produces the identical image. A second draw is the only thing
|
||||||
|
// that can tell a working pipeline apart from a poisoned cache entry: if the first
|
||||||
|
// creation had failed and been memoized, the second draw is where the null would be
|
||||||
|
// served back.
|
||||||
|
//
|
||||||
|
// A deterministic driver-side pipeline-creation FAILURE is not reachable from the GL API on
|
||||||
|
// the llvmpipe/lavapipe lanes - both accept every pipeline these scenarios can describe - so
|
||||||
|
// the guard itself is proven structurally (PipelineFactory returns before it can emplace a
|
||||||
|
// VK_NULL_HANDLE, SetupDraw returns false before it can bind one) and this scenario holds
|
||||||
|
// the surrounding path honest.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Lifted from KHR-GL33.shaders.struct.uniform.sampler_array_vertex (the QPA records the
|
||||||
|
// source verbatim): an array of structs, each carrying an opaque sampler, sampled in the
|
||||||
|
// vertex stage. The fragment sibling of this case only FAILS on Magma; only the vertex one
|
||||||
|
// takes the process down, so the stage matters and is kept.
|
||||||
|
constexpr const char* kSamplerArrayVertexSource = R"(#version 330 core
|
||||||
|
struct S {
|
||||||
|
float a;
|
||||||
|
vec3 b;
|
||||||
|
sampler2D c;
|
||||||
|
};
|
||||||
|
uniform S s[2];
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec4 vColor;
|
||||||
|
void main() {
|
||||||
|
vec2 coords = aPos * 0.5 + 0.5;
|
||||||
|
vColor = vec4(texture(s[1].c, coords * s[0].b.xy + s[1].b.z).rgb, s[0].a);
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kPassthroughFragmentSource = R"(#version 330 core
|
||||||
|
in vec4 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vColor;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vertex> FullscreenTriangleStrip() {
|
||||||
|
return {{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, 1.0f}};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PipelineFailureScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kSamplerArrayVertexSource, kPassthroughFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
const std::vector<Vertex> vertices = FullscreenTriangleStrip();
|
||||||
|
m_vertexCount = static_cast<int>(vertices.size());
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
// A solid red 2x2 texture, so the sampled colour is the same wherever the
|
||||||
|
// (deliberately degenerate) coordinates land.
|
||||||
|
const unsigned char red[] = {255, 0, 0, 255, 255, 0, 0, 255,
|
||||||
|
255, 0, 0, 255, 255, 0, 0, 255};
|
||||||
|
glGenTextures(1, &m_texture);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, red);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glUseProgram(m_program);
|
||||||
|
const int samplerLocation = glGetUniformLocation(m_program, "s[1].c");
|
||||||
|
if (samplerLocation >= 0) glUniform1i(samplerLocation, 0);
|
||||||
|
const int alphaLocation = glGetUniformLocation(m_program, "s[0].a");
|
||||||
|
if (alphaLocation >= 0) glUniform1f(alphaLocation, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
m_target = MakeColorFbo(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_NE(m_target.fbo, 0u) << "offscreen FBO is not framebuffer-complete";
|
||||||
|
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DestroyColorFbo(m_target);
|
||||||
|
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image DrawOnce() {
|
||||||
|
BindFbo(m_target);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, m_vertexCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
return ReadPixels(m_target.width, m_target.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
unsigned int m_texture = 0;
|
||||||
|
int m_vertexCount = 0;
|
||||||
|
ColorFbo m_target;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reaching the assertion at all is most of the point: the shipped code SIGSEGV'd inside
|
||||||
|
// the driver on this draw.
|
||||||
|
TEST_F(PipelineFailureScenario, SamplerArrayInAStructDrawsWithoutKillingTheProcess) {
|
||||||
|
const Image drawn = DrawOnce();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(drawn, 2, drawn.Width() - 3, 2, drawn.Height() - 3, "red", 0.0,
|
||||||
|
"sampler-array-in-struct draw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second draw is what a poisoned cache entry cannot survive: a memoized
|
||||||
|
// VK_NULL_HANDLE is served on every subsequent lookup, so a run that dies (or silently
|
||||||
|
// stops drawing) on the second draw and not the first is exactly the "failed pipeline was
|
||||||
|
// cached" defect.
|
||||||
|
TEST_F(PipelineFailureScenario, TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache) {
|
||||||
|
const Image first = DrawOnce();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the first draw already errored";
|
||||||
|
Gl().EndFrame();
|
||||||
|
const Image second = DrawOnce();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the second draw errored";
|
||||||
|
|
||||||
|
EXPECT_TRUE(RegionIsMostly(second, 2, second.Width() - 3, 2, second.Height() - 3, "red", 0.0,
|
||||||
|
"second draw"));
|
||||||
|
EXPECT_TRUE(second == first) << "the second draw differs from the first in "
|
||||||
|
<< second.ByteDiffCount(first) << " bytes - the pipeline the second "
|
||||||
|
"draw resolved is not the one the first draw used";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PixelStoreSweepScenario.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 - PIXEL-STORE MODES RESTORE, and FRAMEBUFFER CHURN STAYS EXACT.
|
||||||
|
//
|
||||||
|
// Both cases here replay the shape of KHR-GL3x.packed_pixels.varied_rectangle, the single
|
||||||
|
// heaviest polluter in the GL CTS: for each of 46 (pixel-store mode, value) pairs it uploads a
|
||||||
|
// gradient into a fresh texture, attaches that texture to a FRESH framebuffer, reads it back and
|
||||||
|
// deletes both - ~3300 texture+framebuffer pairs per test case.
|
||||||
|
//
|
||||||
|
// What that found: DirectGLES had no destructor for BackendFramebufferObject (nor for the
|
||||||
|
// renderbuffer and sampler twins), so every frontend glDeleteFramebuffers leaked one driver
|
||||||
|
// framebuffer for the process lifetime. On an Adreno 830 the CTS run walked the driver to 1.2 GB
|
||||||
|
// of dead objects, and from that point on EVERY readback through a freshly attached framebuffer
|
||||||
|
// came back with someone else's pixels - which is what made ~1,500 otherwise-correct cases fail
|
||||||
|
// depending only on how much ran before them. The unit-level pin for the missing destructors is
|
||||||
|
// MG_Test/SanityTest.cpp (DirectGLESBackendFramebuffer/Renderbuffer/Sampler); this file pins the
|
||||||
|
// end-to-end behaviour they protect.
|
||||||
|
//
|
||||||
|
// The mode sweep is the second half of the same story: 46 modes are set and reset per case, so a
|
||||||
|
// mode that fails to restore is indistinguishable from the leak in a full-batch CTS run. The
|
||||||
|
// assertion here is RESTORATION - after every single mode is set and put back, a readback at
|
||||||
|
// default state must be byte-identical to one taken before the sweep ever started.
|
||||||
|
//
|
||||||
|
// Backend-agnostic on purpose: both bugs this guards against are frontend/backend bookkeeping,
|
||||||
|
// and DirectVulkan is the built-in control.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Small enough that the table's row lengths (10, 15) and image heights are all >= the
|
||||||
|
// image, which is the shape the CTS uses (its gradient is 7x3).
|
||||||
|
constexpr int kTexSize = 8;
|
||||||
|
// Every buffer handed to GL is this big regardless of the image size: with row length 15,
|
||||||
|
// two skipped rows/pixels and alignment 8 the driver strides well past the natural image
|
||||||
|
// extent, and a tight buffer would be an out-of-bounds access rather than a test. (It was:
|
||||||
|
// the first version of this scenario passed its assertions and then segfaulted at
|
||||||
|
// teardown, because glReadPixels had written past a 1 KiB destination.)
|
||||||
|
constexpr std::size_t kScratchBytes = 64 * 1024;
|
||||||
|
|
||||||
|
// Every pixel-store mode GL 4.0 has, so a reset provably covers the whole state and not
|
||||||
|
// just the subset a particular test happened to touch.
|
||||||
|
struct PixelStoreMode {
|
||||||
|
GLenum name;
|
||||||
|
GLint defaultValue;
|
||||||
|
};
|
||||||
|
const PixelStoreMode kAllModes[] = {
|
||||||
|
{GL_UNPACK_SWAP_BYTES, 0}, {GL_UNPACK_LSB_FIRST, 0}, {GL_UNPACK_ROW_LENGTH, 0},
|
||||||
|
{GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_PIXELS, 0},
|
||||||
|
{GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_ALIGNMENT, 4}, {GL_PACK_SWAP_BYTES, 0},
|
||||||
|
{GL_PACK_LSB_FIRST, 0}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_IMAGE_HEIGHT, 0},
|
||||||
|
{GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_IMAGES, 0},
|
||||||
|
{GL_PACK_ALIGNMENT, 4},
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CTS table verbatim (glcPackedPixelsTests.cpp VariedRectangleTest::iterate): 32
|
||||||
|
// common cases plus the 14 core-only ones ES has no equivalent for and MobileGL therefore
|
||||||
|
// honours on the CPU. IMAGE_WIDTH_1/2 and IMAGE_HEIGHT_1/2 are the CTS's 10 and 15.
|
||||||
|
struct SweepCase {
|
||||||
|
GLenum mode;
|
||||||
|
GLint value;
|
||||||
|
};
|
||||||
|
const SweepCase kSweep[] = {
|
||||||
|
{GL_UNPACK_ROW_LENGTH, 0}, {GL_UNPACK_ROW_LENGTH, 10}, {GL_UNPACK_ROW_LENGTH, 15},
|
||||||
|
{GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_ROWS, 1}, {GL_UNPACK_SKIP_ROWS, 2},
|
||||||
|
{GL_UNPACK_SKIP_PIXELS, 0}, {GL_UNPACK_SKIP_PIXELS, 1}, {GL_UNPACK_SKIP_PIXELS, 2},
|
||||||
|
{GL_UNPACK_ALIGNMENT, 1}, {GL_UNPACK_ALIGNMENT, 2}, {GL_UNPACK_ALIGNMENT, 4},
|
||||||
|
{GL_UNPACK_ALIGNMENT, 8}, {GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_IMAGE_HEIGHT, 10},
|
||||||
|
{GL_UNPACK_IMAGE_HEIGHT, 15}, {GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_SKIP_IMAGES, 1},
|
||||||
|
{GL_UNPACK_SKIP_IMAGES, 2}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_ROW_LENGTH, 10},
|
||||||
|
{GL_PACK_ROW_LENGTH, 15}, {GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_ROWS, 1},
|
||||||
|
{GL_PACK_SKIP_ROWS, 2}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_PIXELS, 1},
|
||||||
|
{GL_PACK_SKIP_PIXELS, 2}, {GL_PACK_ALIGNMENT, 1}, {GL_PACK_ALIGNMENT, 2},
|
||||||
|
{GL_PACK_ALIGNMENT, 4}, {GL_PACK_ALIGNMENT, 8},
|
||||||
|
// core-only, no ES equivalent
|
||||||
|
{GL_UNPACK_SWAP_BYTES, GL_FALSE}, {GL_UNPACK_SWAP_BYTES, GL_TRUE},
|
||||||
|
{GL_UNPACK_LSB_FIRST, GL_FALSE}, {GL_UNPACK_LSB_FIRST, GL_TRUE},
|
||||||
|
{GL_PACK_SWAP_BYTES, GL_FALSE}, {GL_PACK_SWAP_BYTES, GL_TRUE},
|
||||||
|
{GL_PACK_LSB_FIRST, GL_FALSE}, {GL_PACK_LSB_FIRST, GL_TRUE},
|
||||||
|
{GL_PACK_IMAGE_HEIGHT, 0}, {GL_PACK_IMAGE_HEIGHT, 10},
|
||||||
|
{GL_PACK_IMAGE_HEIGHT, 15}, {GL_PACK_SKIP_IMAGES, 0},
|
||||||
|
{GL_PACK_SKIP_IMAGES, 1}, {GL_PACK_SKIP_IMAGES, 2},
|
||||||
|
};
|
||||||
|
|
||||||
|
std::size_t ImageBytes(int size) { return static_cast<std::size_t>(size) * size * 4; }
|
||||||
|
|
||||||
|
// Padded to kScratchBytes so it is safe to hand to an upload running under any of the
|
||||||
|
// sweep's stride/skip settings.
|
||||||
|
std::vector<std::uint8_t> MakeGradient(int size, unsigned seed) {
|
||||||
|
std::vector<std::uint8_t> pixels(kScratchBytes, 0);
|
||||||
|
for (int y = 0; y < size; ++y) {
|
||||||
|
for (int x = 0; x < size; ++x) {
|
||||||
|
const std::size_t base = (static_cast<std::size_t>(y) * size + x) * 4;
|
||||||
|
pixels[base + 0] = static_cast<std::uint8_t>((x * 11 + seed) & 0xFF);
|
||||||
|
pixels[base + 1] = static_cast<std::uint8_t>((y * 13 + seed) & 0xFF);
|
||||||
|
pixels[base + 2] = static_cast<std::uint8_t>((x * y + seed) & 0xFF);
|
||||||
|
pixels[base + 3] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetAllPixelStoreModes() {
|
||||||
|
for (const PixelStoreMode& mode : kAllModes) {
|
||||||
|
glPixelStorei(mode.name, mode.defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one operation the CTS repeats: a fresh texture, a fresh framebuffer, one readback,
|
||||||
|
// both deleted. Returns the readback; `outStatus` carries the completeness answer so a
|
||||||
|
// caller can tell an incomplete framebuffer apart from wrong pixels.
|
||||||
|
std::vector<std::uint8_t> UploadAndReadBack(const std::vector<std::uint8_t>& source, int size,
|
||||||
|
GLenum* outStatus) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, source.data());
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
*outStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> read(kScratchBytes, 0);
|
||||||
|
if (*outStatus == GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index of the first differing byte within the image, or `bytes` when they agree.
|
||||||
|
std::size_t FirstDifference(const std::vector<std::uint8_t>& a, const std::vector<std::uint8_t>& b,
|
||||||
|
std::size_t bytes) {
|
||||||
|
for (std::size_t i = 0; i < bytes; ++i) {
|
||||||
|
if (a[i] != b[i]) return i;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PixelStoreSweepScenario : public ScenarioTest {};
|
||||||
|
class FramebufferChurnScenario : public ScenarioTest {};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Every mode in the CTS table is set, exercised and put back; the readback at default state
|
||||||
|
// afterwards must be bit-identical to the one taken before the sweep. A mode that silently
|
||||||
|
// fails to restore corrupts every later case in the batch, which is exactly how the CTS
|
||||||
|
// failures presented (the FIRST sub-case, at default state, is what failed).
|
||||||
|
TEST_F(PixelStoreSweepScenario, DefaultStateSurvivesTheFullModeSweep) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "resetting the pixel-store modes must be legal on a GL 4.0 context";
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> gradient = MakeGradient(kTexSize, 0);
|
||||||
|
GLenum status = 0;
|
||||||
|
const std::vector<std::uint8_t> baseline = UploadAndReadBack(gradient, kTexSize, &status);
|
||||||
|
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> scratchSource(kScratchBytes, 0x5A);
|
||||||
|
|
||||||
|
for (const SweepCase& sweep : kSweep) {
|
||||||
|
glPixelStorei(sweep.mode, sweep.value);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glPixelStorei(0x" << std::hex << sweep.mode << std::dec << ", "
|
||||||
|
<< sweep.value << ") must be accepted";
|
||||||
|
|
||||||
|
// Exercise the mode: an upload and a readback that both run with it in force.
|
||||||
|
GLenum sweepStatus = 0;
|
||||||
|
(void)UploadAndReadBack(scratchSource, kTexSize, &sweepStatus);
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
|
||||||
|
GLenum afterStatus = 0;
|
||||||
|
const std::vector<std::uint8_t> after = UploadAndReadBack(gradient, kTexSize, &afterStatus);
|
||||||
|
ASSERT_EQ(afterStatus, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
const std::size_t diff = FirstDifference(baseline, after, ImageBytes(kTexSize));
|
||||||
|
ASSERT_EQ(diff, ImageBytes(kTexSize))
|
||||||
|
<< "default-state readback changed after setting and resetting 0x" << std::hex << sweep.mode
|
||||||
|
<< std::dec << " = " << sweep.value << "; first differing byte " << diff << " (baseline "
|
||||||
|
<< static_cast<int>(baseline[diff]) << ", now " << static_cast<int>(after[diff]) << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the modes themselves must read back as the defaults the reset asked for.
|
||||||
|
for (const PixelStoreMode& mode : kAllModes) {
|
||||||
|
GLint value = -1;
|
||||||
|
glGetIntegerv(mode.name, &value);
|
||||||
|
EXPECT_EQ(value, mode.defaultValue)
|
||||||
|
<< "pixel-store mode 0x" << std::hex << mode.name << std::dec << " did not return to its default";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The leak regression. Each iteration is one complete CTS inner step, and every readback has
|
||||||
|
// to be exactly the gradient THIS iteration uploaded - never the previous one's. Before the
|
||||||
|
// missing destructors were added, the driver-side framebuffer count grew without bound here.
|
||||||
|
TEST_F(FramebufferChurnScenario, RepeatedFramebufferReadbackStaysExact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
ResetAllPixelStoreModes();
|
||||||
|
constexpr int kSize = 8;
|
||||||
|
constexpr int kIterations = 1024;
|
||||||
|
|
||||||
|
for (int i = 0; i < kIterations; ++i) {
|
||||||
|
// A distinct gradient per iteration: a stale attachment or a recycled driver name
|
||||||
|
// reads back the PREVIOUS iteration's image, which a constant fill could not tell
|
||||||
|
// apart from a correct read.
|
||||||
|
const std::vector<std::uint8_t> gradient = MakeGradient(kSize, static_cast<unsigned>(i * 7 + 1));
|
||||||
|
GLenum status = 0;
|
||||||
|
const std::vector<std::uint8_t> read = UploadAndReadBack(gradient, kSize, &status);
|
||||||
|
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE)) << "iteration " << i;
|
||||||
|
const std::size_t diff = FirstDifference(gradient, read, ImageBytes(kSize));
|
||||||
|
ASSERT_EQ(diff, ImageBytes(kSize))
|
||||||
|
<< "iteration " << i << " read back a different image than it uploaded; first differing byte "
|
||||||
|
<< diff << " (uploaded " << static_cast<int>(gradient[diff]) << ", read "
|
||||||
|
<< static_cast<int>(read[diff]) << ")";
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "iteration " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,887 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ProgramPipelineScenario.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 - SEPARABLE PROGRAMS DRAWN THROUGH A PROGRAM PIPELINE OBJECT.
|
||||||
|
//
|
||||||
|
// A pipeline object holds one program per stage and stands in for glUseProgram; MobileGL
|
||||||
|
// flattens it into a single composite program at draw time (MG_State/GLState/Core.cpp,
|
||||||
|
// GetProgramForDraw). Sixteen conformance cases across three different families depend on that
|
||||||
|
// flattening and fail identically on BOTH backends - so the defect is in the shared frontend, not
|
||||||
|
// in either backend's draw path:
|
||||||
|
//
|
||||||
|
// compute_shader.{build-monolithic, build-separable, sso-case2, sso-case3, sso-compute-pipeline}
|
||||||
|
// shader_image_load_store.advanced-sso-{atomicCounters, simple, subroutine}
|
||||||
|
// shader_storage_buffer_object.{basic-syntaxSSO, basic-noBindingLayout}
|
||||||
|
//
|
||||||
|
// They fail with two symptoms at once - the draw renders nothing, AND the case leaves a
|
||||||
|
// GL_INVALID_OPERATION behind that the harness reports as "forcing FAIL for subcase". Anything
|
||||||
|
// claiming to be the root cause has to explain both.
|
||||||
|
//
|
||||||
|
// The cases here are the conformance shapes reduced to what fails in milliseconds, ordered from
|
||||||
|
// the simplest pipeline that can render at all up to the compute-then-draw shape of
|
||||||
|
// sso-compute-pipeline. Each one also asserts glGetError is clean at the end, because a case that
|
||||||
|
// paints correctly and leaks an error still fails conformance.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Separable stage sources. A separable VS must redeclare gl_PerVertex, which is exactly
|
||||||
|
// the kind of thing a flattening step can drop on the floor.
|
||||||
|
constexpr const char* kSeparableVS = R"(#version 430 core
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
case 3: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kSeparableFS = R"(#version 430 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The sso-compute-pipeline shape: a compute stage writes the vertex positions the vertex
|
||||||
|
// stage then reads as an attribute, all from one pipeline object.
|
||||||
|
constexpr const char* kComputeSource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std430, binding = 0) buffer Positions {
|
||||||
|
vec4 g_position[4];
|
||||||
|
};
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
g_position[0] = vec4(-1.0, -1.0, 0.0, 1.0);
|
||||||
|
g_position[1] = vec4( 1.0, -1.0, 0.0, 1.0);
|
||||||
|
g_position[2] = vec4(-1.0, 1.0, 0.0, 1.0);
|
||||||
|
g_position[3] = vec4( 1.0, 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kAttributeVS = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 i_position;
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
void main() { gl_Position = i_position; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Two shader storage blocks with NO layout(binding) qualifier, so the only thing that
|
||||||
|
// can say where they live is glShaderStorageBlockBinding - which is per-PROGRAM state.
|
||||||
|
constexpr const char* kStorageBlockVS = R"(#version 430 core
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
layout(std430) buffer Output0 { uint value0; };
|
||||||
|
layout(std430) buffer Output1 { uint value1; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
value0 = 11u;
|
||||||
|
value1 = 22u;
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ProgramPipelineScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindProgramPipeline(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||||
|
for (GLuint p : m_pipelines) glDeleteProgramPipelines(1, &p);
|
||||||
|
m_programs.clear();
|
||||||
|
m_pipelines.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeSeparable(GLenum stage, const char* source) {
|
||||||
|
const GLuint program = glCreateShaderProgramv(stage, 1, &source);
|
||||||
|
if (program != 0) m_programs.push_back(program);
|
||||||
|
// Checked here rather than only at the end of the case: glCreateShaderProgramv is
|
||||||
|
// specified as a sequence of other entry points, so it is the most likely place
|
||||||
|
// for one of them to leave an error nobody consumes.
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u)
|
||||||
|
<< "glCreateShaderProgramv(stage 0x" << std::hex << stage << std::dec << ") left a GL error";
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "glCreateShaderProgramv(stage 0x" << std::hex << stage << std::dec
|
||||||
|
<< ") did not link: " << log;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakePipeline() {
|
||||||
|
GLuint pipeline = 0;
|
||||||
|
glGenProgramPipelines(1, &pipeline);
|
||||||
|
m_pipelines.push_back(pipeline);
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::vector<GLuint> m_pipelines;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The root cause of the cluster, stated as the two halves it actually has.
|
||||||
|
//
|
||||||
|
// Half one: glGenProgramPipelines only reserves a name, and every pipeline command used to
|
||||||
|
// demand a materialized object - so the spec's own call order (stages attached BEFORE the
|
||||||
|
// first bind, GL 4.6 core 7.4) was rejected with GL_INVALID_OPERATION and the stages were
|
||||||
|
// never recorded. Half two is the trap that fix walks into: the object now appears the
|
||||||
|
// moment anything needs somewhere to put state, so "the object exists" stops being the
|
||||||
|
// right answer for glIsProgramPipeline, which the spec ties to the first BIND. A pure
|
||||||
|
// query must not turn a reserved name into a program pipeline either.
|
||||||
|
TEST_F(ProgramPipelineScenario, AReservedNameTakesStateBeforeItIsAProgramPipeline) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
if (vs == 0) return;
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
ASSERT_NE(pipeline, 0u);
|
||||||
|
EXPECT_EQ(glIsProgramPipeline(pipeline), GL_FALSE) << "a merely reserved name is not a pipeline yet";
|
||||||
|
|
||||||
|
// A query answers out of default state - and leaves the name exactly as it found it.
|
||||||
|
GLint validateStatus = -1;
|
||||||
|
glGetProgramPipelineiv(pipeline, GL_VALIDATE_STATUS, &validateStatus);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "querying a reserved pipeline name must not be an error";
|
||||||
|
EXPECT_EQ(validateStatus, 0) << "a pipeline that was never validated reports VALIDATE_STATUS 0";
|
||||||
|
EXPECT_EQ(glIsProgramPipeline(pipeline), GL_FALSE) << "a pure query must not create the object";
|
||||||
|
|
||||||
|
// ...and glUseProgramStages RECORDS the stage on the reserved name rather than
|
||||||
|
// rejecting it, which is the whole defect: without this the pipeline stayed empty.
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glUseProgramStages before the first bind must be accepted";
|
||||||
|
GLint stageProgram = 0;
|
||||||
|
glGetProgramPipelineiv(pipeline, GL_VERTEX_SHADER, &stageProgram);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(stageProgram), vs) << "the stage program was not recorded";
|
||||||
|
EXPECT_EQ(glIsProgramPipeline(pipeline), GL_FALSE) << "taking state is still not being bound";
|
||||||
|
|
||||||
|
// The bind is what the spec ties glIsProgramPipeline to.
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
EXPECT_EQ(glIsProgramPipeline(pipeline), GL_TRUE);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
glBindProgramPipeline(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The floor: a two-stage pipeline must paint. If this fails, nothing above it can pass, and
|
||||||
|
// the eight shared conformance cases have exactly one cause.
|
||||||
|
TEST_F(ProgramPipelineScenario, ATwoStagePipelinePaintsWhatItsStagesDescribe) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSeparableFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "pipeline setup left a GL error behind";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
// No glUseProgram anywhere: the pipeline IS the program state for this draw.
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a two-stage program pipeline drawing a full-viewport strip"));
|
||||||
|
// The conformance harness fails a subcase on a leaked error even when the pixels are
|
||||||
|
// right, so this assertion is not redundant with the one above.
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// glActiveShaderProgram picks which stage program glUniform* addresses - and the draw has to
|
||||||
|
// see what was written there.
|
||||||
|
//
|
||||||
|
// The second defect of the cluster, and the one the pixels expose most directly: uniform
|
||||||
|
// values live on the stage program (GetProgramForUniform returns the pipeline's active
|
||||||
|
// program) while the draw reads the composite GetProgramForDraw builds out of the stage
|
||||||
|
// programs' shaders. Two objects, two sets of uniform storage; before the composite was
|
||||||
|
// refreshed from its stage programs this painted u_color's zero default instead of green.
|
||||||
|
TEST_F(ProgramPipelineScenario, UniformsGoToTheActiveShaderProgram) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
static const char* kUniformFS = R"(#version 430 core
|
||||||
|
uniform vec4 u_color;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = u_color; }
|
||||||
|
)";
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kUniformFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glActiveShaderProgram(pipeline, fs);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glActiveShaderProgram left a GL error behind";
|
||||||
|
|
||||||
|
const GLint location = glGetUniformLocation(fs, "u_color");
|
||||||
|
ASSERT_NE(location, -1);
|
||||||
|
glUniform4f(location, 0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glUniform4f through the active shader program errored";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a pipeline whose fragment uniform was set via glActiveShaderProgram"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sso-compute-pipeline shape: compute and non-compute stages on ONE pipeline object, the
|
||||||
|
// compute stage writing the buffer the vertex stage then reads.
|
||||||
|
//
|
||||||
|
// The third defect of the cluster: the flattening used to pull EVERY stage into one
|
||||||
|
// composite, so a single program was asked to serve both glDispatchCompute and glDrawArrays.
|
||||||
|
// GL keeps them apart - a pipeline's compute stage is a whole program dispatched on its own
|
||||||
|
// and never participates in a draw - which is why the accessors are split (GetProgramForDraw
|
||||||
|
// composites the graphics stages, GetProgramForDispatch hands back the compute stage
|
||||||
|
// program). It is also the shape that killed the process on Adreno: the composite carried a
|
||||||
|
// compute module into vkCreateGraphicsPipelines, and that driver SIGSEGVs rather than
|
||||||
|
// returning an error.
|
||||||
|
TEST_F(ProgramPipelineScenario, ComputeAndGraphicsStagesShareOnePipeline) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
GLint storageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &storageBlocks);
|
||||||
|
if (storageBlocks < 1) {
|
||||||
|
GTEST_SKIP() << "no compute shader storage blocks available";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint cs = MakeSeparable(GL_COMPUTE_SHADER, kComputeSource);
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kAttributeVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSeparableFS);
|
||||||
|
if (cs == 0 || vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
glUseProgramStages(pipeline, GL_COMPUTE_SHADER_BIT, cs);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching compute and graphics stages to one pipeline errored";
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, 4 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||||
|
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffer);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a pipeline whose compute stage wrote the vertex positions"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the compute-then-draw pipeline leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface-resource bindings are per-PROGRAM state, and the program a pipeline draw executes
|
||||||
|
// is the composite - not the stage program the application set them on.
|
||||||
|
//
|
||||||
|
// This is shader_storage_buffer_object.basic-noBindingLayout reduced: blocks declared without
|
||||||
|
// a layout(binding) qualifier, placed onto binding points purely by
|
||||||
|
// glShaderStorageBlockBinding against the stage program. The stage program records the
|
||||||
|
// rebinding (ProgramObject::SetShaderStorageBlockBinding, keyed by block name) and the
|
||||||
|
// composite is built from the stage program's SHADERS - which carry the declared bindings and
|
||||||
|
// know nothing of the rebinding. So the draw writes wherever the shader source said, the
|
||||||
|
// bound buffer ranges never see a byte, and no GL error is raised anywhere: the readback is
|
||||||
|
// the only thing that notices.
|
||||||
|
TEST_F(ProgramPipelineScenario, AStageProgramsStorageBlockBindingReachesThePipelineDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLint vertexStorageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &vertexStorageBlocks);
|
||||||
|
if (vertexStorageBlocks < 2) {
|
||||||
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kStorageBlockVS);
|
||||||
|
if (vs == 0) return;
|
||||||
|
|
||||||
|
// Rebound to binding points the shader source never mentions, so nothing but the
|
||||||
|
// rebinding can put the writes where this case looks for them.
|
||||||
|
constexpr GLuint kBinding0 = 1;
|
||||||
|
constexpr GLuint kBinding1 = 5;
|
||||||
|
const GLuint block0 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output0");
|
||||||
|
const GLuint block1 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output1");
|
||||||
|
ASSERT_NE(block0, GL_INVALID_INDEX);
|
||||||
|
ASSERT_NE(block1, GL_INVALID_INDEX);
|
||||||
|
glShaderStorageBlockBinding(vs, block0, kBinding0);
|
||||||
|
glShaderStorageBlockBinding(vs, block1, kBinding1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glShaderStorageBlockBinding on a separable program errored";
|
||||||
|
|
||||||
|
GLint offsetAlignment = 256;
|
||||||
|
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment <= 0) offsetAlignment = 256;
|
||||||
|
const GLsizeiptr secondOffset = offsetAlignment;
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(secondOffset) / sizeof(GLuint) + 4, 0u);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(zeros.size() * sizeof(GLuint)), zeros.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding0, buffer, 0, sizeof(GLuint));
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding1, buffer, secondOffset, sizeof(GLuint));
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
// The whole point is the buffer writes, so the rasterizer is not involved - which is
|
||||||
|
// also what keeps a vertex-only pipeline (no fragment stage) legal here.
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the storage-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
GLuint readback0 = 0;
|
||||||
|
GLuint readback1 = 0;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(readback0), &readback0);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, secondOffset, sizeof(readback1), &readback1);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(readback0, 11u) << "Output0 did not reach the binding glShaderStorageBlockBinding gave it";
|
||||||
|
EXPECT_EQ(readback1, 22u) << "Output1 did not reach the binding glShaderStorageBlockBinding gave it";
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL for the case above, and the thing that says whether a storage-block failure is
|
||||||
|
// about pipelines at all: the same shader, the same rebinding, in an ordinary two-stage
|
||||||
|
// monolithic program run through glUseProgram. If this one fails too then the composite is
|
||||||
|
// innocent and the defect is in how the backend replays a rebinding.
|
||||||
|
//
|
||||||
|
// Two stages on purpose. Handing glUseProgram a vertex-ONLY program would confound the
|
||||||
|
// experiment - a program with no fragment stage is a thing some backends cannot build at
|
||||||
|
// all, so its failure would say nothing about block bindings.
|
||||||
|
//
|
||||||
|
// Runs on both backends. glShaderStorageBlockBinding is a GL 4.3 entry point with no ES
|
||||||
|
// equivalent - ES fixes a storage block's binding at link from its layout(binding=)
|
||||||
|
// qualifier - so Espryt honours a rebinding by writing the effective binding into the ESSL
|
||||||
|
// it generates (the Binding decoration is rewritten before SPIRV-Cross emits, and the draw
|
||||||
|
// path rebuilds a program whose override set has moved).
|
||||||
|
TEST_F(ProgramPipelineScenario, AStorageBlockRebindingHoldsWithoutAPipeline) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLint vertexStorageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &vertexStorageBlocks);
|
||||||
|
if (vertexStorageBlocks < 2) {
|
||||||
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kMonolithicVS = R"(#version 430 core
|
||||||
|
layout(std430) buffer Output0 { uint value0; };
|
||||||
|
layout(std430) buffer Output1 { uint value1; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
value0 = 11u;
|
||||||
|
value1 = 22u;
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
static const char* kMonolithicFS = R"(#version 430 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
std::string compileError;
|
||||||
|
const GLuint vs = CompileProgram(kMonolithicVS, kMonolithicFS, &compileError);
|
||||||
|
ASSERT_NE(vs, 0u) << compileError;
|
||||||
|
m_programs.push_back(vs);
|
||||||
|
|
||||||
|
constexpr GLuint kBinding0 = 1;
|
||||||
|
constexpr GLuint kBinding1 = 5;
|
||||||
|
const GLuint block0 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output0");
|
||||||
|
const GLuint block1 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output1");
|
||||||
|
ASSERT_NE(block0, GL_INVALID_INDEX);
|
||||||
|
ASSERT_NE(block1, GL_INVALID_INDEX);
|
||||||
|
glShaderStorageBlockBinding(vs, block0, kBinding0);
|
||||||
|
glShaderStorageBlockBinding(vs, block1, kBinding1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
GLint offsetAlignment = 256;
|
||||||
|
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment <= 0) offsetAlignment = 256;
|
||||||
|
const GLsizeiptr secondOffset = offsetAlignment;
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(secondOffset) / sizeof(GLuint) + 4, 0u);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(zeros.size() * sizeof(GLuint)), zeros.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding0, buffer, 0, sizeof(GLuint));
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding1, buffer, secondOffset, sizeof(GLuint));
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
// No pipeline anywhere: a separable program is still a perfectly good current program.
|
||||||
|
glBindProgramPipeline(0);
|
||||||
|
glUseProgram(vs);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the monolithic storage-block draw leaked a GL error";
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
GLuint readback0 = 0;
|
||||||
|
GLuint readback1 = 0;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(readback0), &readback0);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, secondOffset, sizeof(readback1), &readback1);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(readback0, 11u) << "Output0 missed its rebinding with no pipeline involved";
|
||||||
|
EXPECT_EQ(readback1, 22u) << "Output1 missed its rebinding with no pipeline involved";
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same defect through the other block flavour: glUniformBlockBinding is also per-program
|
||||||
|
// state, recorded on the stage program by GL block index, and also never reaches the
|
||||||
|
// composite the draw actually runs.
|
||||||
|
TEST_F(ProgramPipelineScenario, AStageProgramsUniformBlockBindingReachesThePipelineDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
static const char* kUniformBlockFS = R"(#version 430 core
|
||||||
|
layout(std140) uniform Colour { vec4 u_colour; };
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = u_colour; }
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kUniformBlockFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
constexpr GLuint kBinding = 3; // not the default 0 the declaration implies
|
||||||
|
const GLuint blockIndex = glGetUniformBlockIndex(fs, "Colour");
|
||||||
|
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||||
|
glUniformBlockBinding(fs, blockIndex, kBinding);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glUniformBlockBinding on a separable program errored";
|
||||||
|
|
||||||
|
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, buffer);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, sizeof(green), green, GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kBinding, buffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a pipeline whose fragment uniform block was rebound to binding 3"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the uniform-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shared-header idiom, drawn: BOTH stages declare `u_mvp` because they both include the
|
||||||
|
// same header, and only the VERTEX program is ever written to.
|
||||||
|
//
|
||||||
|
// The composite has one slot for `u_mvp`, and mirroring every active uniform of every stage
|
||||||
|
// in stage order meant the fragment program's untouched zero matrix landed last and won.
|
||||||
|
// The vertex stage then transformed every vertex by a zero matrix and the frame came out
|
||||||
|
// empty - from an application that had done nothing wrong, with no GL error anywhere to say
|
||||||
|
// so. Only uniforms a stage has actually been written to are mirrored now.
|
||||||
|
TEST_F(ProgramPipelineScenario, AUniformDeclaredInTwoStagesKeepsTheValueTheWrittenStageHolds) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
// The same declaration in both stages, exactly as a shared header produces it. The
|
||||||
|
// fragment stage does not even USE it for its output - declaring it is enough.
|
||||||
|
static const char* kSharedMvpVS = R"(#version 430 core
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
uniform mat4 u_mvp;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec4 corner = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: corner = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: corner = vec4( 1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: corner = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
case 3: corner = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
gl_Position = u_mvp * corner;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
static const char* kSharedMvpFS = R"(#version 430 core
|
||||||
|
uniform mat4 u_mvp;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, u_mvp[3][3]); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSharedMvpVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSharedMvpFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
|
||||||
|
// Written through the VERTEX program only - which is the whole point. The fragment
|
||||||
|
// program's `u_mvp` is left at GL's zero default and must not win the composite's slot.
|
||||||
|
glActiveShaderProgram(pipeline, vs);
|
||||||
|
const GLint location = glGetUniformLocation(vs, "u_mvp");
|
||||||
|
ASSERT_NE(location, -1);
|
||||||
|
const GLfloat identity[16] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
glUniformMatrix4fv(location, 1, GL_FALSE, identity);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glUniformMatrix4fv through the active shader program errored";
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
// A zero matrix collapses all four corners onto the origin and paints nothing at all, so
|
||||||
|
// "green over the whole viewport" IS the assertion that the written matrix was the one
|
||||||
|
// the draw used. (The fragment stage reads u_mvp too - into the alpha channel - purely
|
||||||
|
// so the optimizer cannot delete its declaration and make the case vacuous.)
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a pipeline whose u_mvp is declared in both stages and written in one"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the shared-uniform pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebinding a uniform block AFTER the pipeline has already drawn once.
|
||||||
|
//
|
||||||
|
// This is the shape the composite cache key change put weight on. The composite used to be
|
||||||
|
// thrown away and relinked whenever glUniformBlockBinding moved a stage program's backend
|
||||||
|
// state version, so the second draw here got a brand-new composite that happened to pick the
|
||||||
|
// new binding up on the way. Now the composite SURVIVES the rebinding, which means the only
|
||||||
|
// thing that can carry the new binding to the draw is the refresh path - so this case is
|
||||||
|
// what says that path is really doing the work.
|
||||||
|
TEST_F(ProgramPipelineScenario, RebindingAUniformBlockBetweenDrawsReachesTheNextDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
static const char* kUniformBlockFS = R"(#version 430 core
|
||||||
|
layout(std140) uniform Colour { vec4 u_colour; };
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = u_colour; }
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kUniformBlockFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
// Two buffers on two different binding points, holding two different colours.
|
||||||
|
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||||
|
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
constexpr GLuint kFirstBinding = 2;
|
||||||
|
constexpr GLuint kSecondBinding = 5;
|
||||||
|
GLuint buffers[2] = {0, 0};
|
||||||
|
glGenBuffers(2, buffers);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, buffers[0]);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, sizeof(red), red, GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kFirstBinding, buffers[0]);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, buffers[1]);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, sizeof(green), green, GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kSecondBinding, buffers[1]);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
const GLuint blockIndex = glGetUniformBlockIndex(fs, "Colour");
|
||||||
|
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||||
|
glUniformBlockBinding(fs, blockIndex, kFirstBinding);
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
|
||||||
|
// Draw one: the composite is built here, against binding 2.
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
const Image first = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(first, 2, width - 3, 2, height - 3, "red", 0.0,
|
||||||
|
"the first pipeline draw, with Colour on binding 2"));
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the first uniform-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
// Move the block to the other binding point, with the composite already built and cached.
|
||||||
|
glUniformBlockBinding(fs, blockIndex, kSecondBinding);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "rebinding a uniform block between draws errored";
|
||||||
|
|
||||||
|
// Draw two must read the OTHER buffer.
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
const Image second = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(second, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"the second pipeline draw, after Colour was rebound to binding 5"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the rebound uniform-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(2, buffers);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sampler-unit half of the same question, in a loop: set a unit, draw, repeat. This is
|
||||||
|
// the shape KHR-GL42.shader_image_load_store.advanced-sso-* and the compute_shader SSO cases
|
||||||
|
// run, and the one that used to relink the composite on every single iteration. The pixels
|
||||||
|
// pin what the loop must PRODUCE; the composite-identity assertion that pins what it must
|
||||||
|
// COST lives in the MG_Test unit suite, where the object itself is reachable.
|
||||||
|
TEST_F(ProgramPipelineScenario, ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
static const char* kSamplerFS = R"(#version 430 core
|
||||||
|
uniform sampler2D u_tex;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = texture(u_tex, vec2(0.5)); }
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSamplerFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
// One texture per unit, each a different solid colour, so the pixels say which unit the
|
||||||
|
// draw actually sampled.
|
||||||
|
constexpr int kUnits = 4;
|
||||||
|
const GLubyte colours[kUnits][4] = {{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}};
|
||||||
|
const char* names[kUnits] = {"red", "green", "blue", "yellow"};
|
||||||
|
GLuint textures[kUnits] = {};
|
||||||
|
glGenTextures(kUnits, textures);
|
||||||
|
for (int unit = 0; unit < kUnits; ++unit) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + unit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textures[unit]);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, colours[unit]);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
}
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup left a GL error behind";
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glActiveShaderProgram(pipeline, fs);
|
||||||
|
const GLint sampler = glGetUniformLocation(fs, "u_tex");
|
||||||
|
ASSERT_NE(sampler, -1);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
for (int unit = 0; unit < kUnits; ++unit) {
|
||||||
|
glUniform1i(sampler, unit);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, names[unit], 0.0,
|
||||||
|
"a pipeline draw after its sampler was pointed at another unit"))
|
||||||
|
<< "unit " << unit;
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the sampler-rewrite pipeline draw leaked a GL error at unit " << unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(kUnits, textures);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// build-separable / build-monolithic reduce to this: a separable program and a monolithic one
|
||||||
|
// must both be usable, and switching between pipeline and glUseProgram must leave no error.
|
||||||
|
TEST_F(ProgramPipelineScenario, SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kSeparableFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT | GL_FRAGMENT_SHADER_BIT, 0);
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const unsigned int monolithic = CompileProgram(
|
||||||
|
"#version 330 core\nin vec2 aPos;\nvoid main(){ gl_Position = vec4(aPos,0.0,1.0); }\n",
|
||||||
|
"#version 330 core\nout vec4 o;\nvoid main(){ o = vec4(1.0,0.0,0.0,1.0); }\n", &error);
|
||||||
|
ASSERT_NE(monolithic, 0u) << error;
|
||||||
|
m_programs.push_back(monolithic);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
|
||||||
|
// GL 4.6 core 7.3: while a program is current, it takes precedence over the pipeline.
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glUseProgram(monolithic);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "drawing with a current program while a pipeline is bound errored";
|
||||||
|
|
||||||
|
// ... and once it is not current, the pipeline takes over again.
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"the pipeline after the current program was unbound"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "switching back to the pipeline leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
} // 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