mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
239
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee124018a2 | ||
|
|
d7ce0c48ef | ||
|
|
eadf7bc474 | ||
|
|
00a326ef78 | ||
|
|
c09045fe59 | ||
|
|
5bd8ef01e5 | ||
|
|
3181ed2c5a | ||
|
|
6aed3b08f3 | ||
|
|
281467a345 | ||
|
|
c7e36986e7 | ||
|
|
d8576a2ed3 | ||
|
|
2b6c2b561c | ||
|
|
12c94111b5 | ||
|
|
7769156cfc | ||
|
|
0ecfdff4e7 | ||
|
|
6df5a6137f | ||
|
|
b3794f4e6a | ||
|
|
14d3901d30 | ||
|
|
d4766513e4 | ||
|
|
72dc7aa6aa | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a | ||
|
|
6a80a82dd3 | ||
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 | ||
|
|
bce34d7fac | ||
|
|
f91857266f | ||
|
|
49cb1be0fd | ||
|
|
a51c68bb2c | ||
|
|
1c723a6cfc | ||
|
|
44805bfa07 | ||
|
|
257fcbfd0b | ||
|
|
2b46a3db96 | ||
|
|
0b36621069 | ||
|
|
9ee2e0a1db | ||
|
|
6b6623ae72 | ||
|
|
a6e029734b | ||
|
|
e7d6bfddac | ||
|
|
f2c879528f | ||
|
|
eaba4ac1dc | ||
|
|
ed6578954e | ||
|
|
e005c8b6cb | ||
|
|
535b5e3095 | ||
|
|
1b05a84928 | ||
|
|
7ccb762936 | ||
|
|
442e7eec1c | ||
|
|
2787d15706 | ||
|
|
74ce58a6c7 | ||
|
|
bb122ebd4f | ||
|
|
9b0ed5b3af | ||
|
|
0e31c1481b | ||
|
|
811f32760e | ||
|
|
01381a0404 | ||
|
|
0f02b0fdb1 | ||
|
|
3be02abf47 | ||
|
|
f91d6b676c | ||
|
|
1ebf191f94 | ||
|
|
ccad803023 | ||
|
|
21159caf31 | ||
|
|
ea4819a21d | ||
|
|
6b882b3ccf | ||
|
|
96bd36c50b | ||
|
|
46fbd837b3 | ||
|
|
796a57a115 | ||
|
|
62a2dae5ba | ||
|
|
532836c058 | ||
|
|
2fced2241b | ||
|
|
21b5fc2d92 | ||
|
|
1f753ab5fa | ||
|
|
3ed9501be5 | ||
|
|
7311251f30 | ||
|
|
7625cf450d | ||
|
|
450eb209b6 | ||
|
|
8c5c39b3c3 | ||
|
|
64a0ea397c | ||
|
|
205d837942 | ||
|
|
b5e9339c66 | ||
|
|
e310e3e9ff | ||
|
|
a0bf4a83bc | ||
|
|
97facf777b | ||
|
|
5cfbb716c0 | ||
|
|
64c3411d70 | ||
|
|
f3a0d9e0a3 | ||
|
|
068786e812 | ||
|
|
8e7cc62c24 | ||
|
|
d2a36d65a3 | ||
|
|
a020de76e3 | ||
|
|
574634adfa | ||
|
|
e71d715e1a | ||
|
|
7b946fd527 | ||
|
|
8f3ce5f5b7 | ||
|
|
21ec744ef2 | ||
|
|
faa7b17da3 | ||
|
|
f6849fc0b3 | ||
|
|
4d1d4f6225 | ||
|
|
cef81df73f | ||
|
|
c4e6ea1f23 | ||
|
|
b4e07ce651 | ||
|
|
ff324057ad | ||
|
|
ef4c6dbe0a | ||
|
|
19fc7346c5 | ||
|
|
bcb0e894ef | ||
|
|
794c10e56c | ||
|
|
28390667d7 | ||
|
|
90dd9bec77 | ||
|
|
7994ca31d3 | ||
|
|
5705e05156 | ||
|
|
ab62f81545 | ||
|
|
f6cf04d6d7 | ||
|
|
38eb9589f9 | ||
|
|
99ebf67a3d | ||
|
|
2292e99476 | ||
|
|
4c5afecc71 | ||
|
|
1c5744f2be | ||
|
|
22859b0958 | ||
|
|
7aa958fbc9 | ||
|
|
a4bd4e04a1 | ||
|
|
09459edb6b | ||
|
|
8af6ebc174 | ||
|
|
577cd8c670 | ||
|
|
5267243404 | ||
|
|
33c2715912 | ||
|
|
3068cdadf8 | ||
|
|
6dd0201bf2 | ||
|
|
05bef7118b | ||
|
|
94e75fef79 | ||
|
|
43bcd03dca | ||
|
|
2ce0595fab | ||
|
|
ba9af18033 | ||
|
|
f7d63f88fa | ||
|
|
6cf5a7744e | ||
|
|
ca3d24f5ea | ||
|
|
cc34d34706 | ||
|
|
757b31592d | ||
|
|
dbae4eda10 | ||
|
|
fa5ff5d168 | ||
|
|
994ae372f8 | ||
|
|
7ba012adf9 | ||
|
|
b1fdffd767 | ||
|
|
18c17ae5ca | ||
|
|
16c010985f | ||
|
|
6f64ec0f51 | ||
|
|
5d47698349 | ||
|
|
7b593e39ef | ||
|
|
d83b4dbbb5 | ||
|
|
964a7fcc92 | ||
|
|
5a7bd9942d | ||
|
|
21a43bf6a4 | ||
|
|
5b6dec2d81 | ||
|
|
543c29bf86 | ||
|
|
ef562ee9b5 | ||
|
|
fa0f6693d0 | ||
|
|
a6c362c6ce | ||
|
|
921504eccf | ||
|
|
7c5fc03b26 | ||
|
|
ed29e63543 | ||
|
|
5c8a9c41d6 | ||
|
|
efa0345c36 | ||
|
|
ce0f18969c | ||
|
|
7ce0966e7d | ||
|
|
1c6ca2753f | ||
|
|
61b0532865 | ||
|
|
f5b8a505ed | ||
|
|
8371365db5 | ||
|
|
b219992ee3 | ||
|
|
94233ef928 | ||
|
|
0827d7a539 | ||
|
|
5248b8b746 | ||
|
|
d868e1c476 | ||
|
|
c3412ca394 | ||
|
|
5ccaff37af | ||
|
|
71e29f9d58 | ||
|
|
8ad07c222c | ||
|
|
5722094d6f | ||
|
|
4831387cf0 | ||
|
|
d03b72267a | ||
|
|
847ec74f48 | ||
|
|
e02e5caa17 | ||
|
|
1958934594 | ||
|
|
dec0c5eaff | ||
|
|
b6a44cd1e2 | ||
|
|
85f45d0e44 | ||
|
|
404236d337 | ||
|
|
6ea948779e | ||
|
|
d8d7530011 | ||
|
|
0f394fa46f | ||
|
|
107669b3db | ||
|
|
3e0460e472 | ||
|
|
33ff177bb2 | ||
|
|
2e6fc1ffc0 | ||
|
|
c6299f754f | ||
|
|
dcf918b9ee | ||
|
|
d98f72447d | ||
|
|
f15cb8900f | ||
|
|
bd0def6133 | ||
|
|
6f8b7fbc40 | ||
|
|
e5fb57f7eb | ||
|
|
c93e5fa409 | ||
|
|
8191075133 | ||
|
|
d6caed7822 | ||
|
|
9152e88734 | ||
|
|
2406e2d219 | ||
|
|
b228f813c0 | ||
|
|
0d0527192a | ||
|
|
81bcbd6c14 | ||
|
|
867fe3e0ef | ||
|
|
0ec487c993 | ||
|
|
23b880c8be | ||
|
|
ebc5bff9b1 | ||
|
|
231d5c90e4 | ||
|
|
d5f5e6405b | ||
|
|
ac3a83b207 | ||
|
|
335f2decbd | ||
|
|
fb1ad96c04 | ||
|
|
313b75a7c0 |
@@ -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'
|
||||||
|
}
|
||||||
+116
-16
@@ -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)"
|
||||||
@@ -185,7 +209,7 @@ jobs:
|
|||||||
- name: Load trace cases
|
- name: Load trace cases
|
||||||
id: trace-cases
|
id: trace-cases
|
||||||
run: |
|
run: |
|
||||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
|
||||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
trace-fixtures:
|
trace-fixtures:
|
||||||
@@ -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')"
|
||||||
@@ -281,13 +337,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||||
backend:
|
|
||||||
- name: DirectGLES
|
|
||||||
gpu: software
|
|
||||||
- name: DirectVulkan
|
|
||||||
gpu: lavapipe
|
|
||||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
uses: pierotofy/set-swap-space@v1.0
|
uses: pierotofy/set-swap-space@v1.0
|
||||||
@@ -370,6 +420,9 @@ jobs:
|
|||||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||||
|
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
|
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
|
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||||
run: |
|
run: |
|
||||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||||
test -f "${apk_file}"
|
test -f "${apk_file}"
|
||||||
@@ -379,6 +432,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 +467,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 +524,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 +612,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})"
|
||||||
|
|||||||
+211
-14
@@ -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,105 @@ 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"
|
||||||
|
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||||
|
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
|
||||||
|
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
|
||||||
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
|
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||||
|
ctest -V -L integration-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 +328,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 +347,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 +375,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 +445,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,7 +492,9 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test
|
- test
|
||||||
- benchmark
|
- benchmark
|
||||||
|
- integration
|
||||||
outputs:
|
outputs:
|
||||||
|
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||||
names: ${{ steps.trace-cases.outputs.names }}
|
names: ${{ steps.trace-cases.outputs.names }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -351,7 +502,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Load trace cases
|
- name: Load trace cases
|
||||||
id: trace-cases
|
id: trace-cases
|
||||||
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
run: |
|
||||||
|
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
trace-fixtures:
|
trace-fixtures:
|
||||||
name: trace fixture (${{ matrix.case }})
|
name: trace fixture (${{ matrix.case }})
|
||||||
@@ -366,9 +519,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')"
|
||||||
@@ -398,11 +583,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
|
||||||
backend:
|
|
||||||
- DirectGLES
|
|
||||||
- DirectVulkan
|
|
||||||
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
@@ -456,9 +637,17 @@ 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
|
||||||
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||||
|
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
||||||
|
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
||||||
|
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
||||||
|
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
||||||
|
fi
|
||||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
||||||
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||||
# bypasses only the vendor gate, so this exercises the real strip on
|
# bypasses only the vendor gate, so this exercises the real strip on
|
||||||
@@ -470,6 +659,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
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
|
|||||||
tools/trace_replay/work/
|
tools/trace_replay/work/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
/.gradle
|
||||||
|
|||||||
+6
-3
@@ -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
|
||||||
@@ -31,3 +28,9 @@
|
|||||||
[submodule "3rdparty/apitrace"]
|
[submodule "3rdparty/apitrace"]
|
||||||
path = 3rdparty/apitrace
|
path = 3rdparty/apitrace
|
||||||
url = https://github.com/MobileGL-Dev/apitrace.git
|
url = https://github.com/MobileGL-Dev/apitrace.git
|
||||||
|
[submodule "3rdparty/asio"]
|
||||||
|
path = 3rdparty/asio
|
||||||
|
url = https://github.com/chriskohlhoff/asio.git
|
||||||
|
[submodule "include/ska"]
|
||||||
|
path = include/ska
|
||||||
|
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: 10935bb5e4...c8036190fc
+1
Submodule 3rdparty/asio added at 8806a6803c
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 900b29d449...6f12598784
+137
-1
@@ -4,6 +4,11 @@ project("MobileGL")
|
|||||||
|
|
||||||
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
|
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
|
||||||
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
|
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
|
||||||
|
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
|
||||||
|
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
|
||||||
|
# skips cleanly where there is none. Registered under the `integration-gpu`
|
||||||
|
# ctest label so a run can select or exclude them.
|
||||||
|
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
|
||||||
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
|
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
|
||||||
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
|
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
|
||||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||||
@@ -15,9 +20,86 @@ 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()
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
|
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||||
|
|
||||||
|
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||||
# Check if ThinLTO or LTO is suppported
|
# Check if ThinLTO or LTO is suppported
|
||||||
include(CheckIPOSupported)
|
include(CheckIPOSupported)
|
||||||
include(CheckCCompilerFlag)
|
include(CheckCCompilerFlag)
|
||||||
@@ -100,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
|||||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||||
|
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||||
|
|
||||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||||
@@ -147,6 +230,9 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
MobileGL/MG_Util/Debug/Log.cpp
|
MobileGL/MG_Util/Debug/Log.cpp
|
||||||
|
|
||||||
|
MobileGL/MG_Util/Async/JobNode.cpp
|
||||||
|
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/Math/VectorTypes.cpp
|
MobileGL/MG_Util/Math/VectorTypes.cpp
|
||||||
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
|
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
|
||||||
|
|
||||||
@@ -180,6 +266,7 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
|
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
|
||||||
|
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||||
@@ -187,20 +274,35 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/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/DeriveNumSubgroupsPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/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
|
||||||
|
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||||
@@ -220,6 +322,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
||||||
|
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||||
@@ -243,6 +346,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||||
|
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||||
|
|
||||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||||
@@ -281,7 +385,12 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||||
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/ProgramSpirvTask.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/ShaderCompileAdoptionMap.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
|
||||||
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
|
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
|
||||||
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
|
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
|
||||||
@@ -315,6 +424,11 @@ if (WIN32)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
|
||||||
|
# merged pthread into libc, so it links without asking, but the NDK and musl are not
|
||||||
|
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
set(MOBILEGL_LINK_LIBRARIES
|
set(MOBILEGL_LINK_LIBRARIES
|
||||||
glslang::glslang
|
glslang::glslang
|
||||||
spirv-cross-c
|
spirv-cross-c
|
||||||
@@ -324,12 +438,17 @@ set(MOBILEGL_LINK_LIBRARIES
|
|||||||
GPUOpen::VulkanMemoryAllocator
|
GPUOpen::VulkanMemoryAllocator
|
||||||
Vulkan::UtilityHeaders
|
Vulkan::UtilityHeaders
|
||||||
spirv-reflect-static
|
spirv-reflect-static
|
||||||
|
Threads::Threads
|
||||||
)
|
)
|
||||||
|
|
||||||
set(MOBILEGL_COMPILE_DEF
|
set(MOBILEGL_COMPILE_DEF
|
||||||
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
||||||
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
||||||
-DVMA_VULKAN_VERSION=1001000
|
-DVMA_VULKAN_VERSION=1001000
|
||||||
|
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
|
||||||
|
# rather than per-target so the shared library and the _s static target agree.
|
||||||
|
-DASIO_STANDALONE
|
||||||
|
-DASIO_NO_DEPRECATED
|
||||||
)
|
)
|
||||||
|
|
||||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||||
@@ -341,6 +460,10 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
${spirv-tools_SOURCE_DIR}/include
|
${spirv-tools_SOURCE_DIR}/include
|
||||||
${spirv-tools_BINARY_DIR}
|
${spirv-tools_BINARY_DIR}
|
||||||
${SPIRV-Headers_SOURCE_DIR}/include
|
${SPIRV-Headers_SOURCE_DIR}/include
|
||||||
|
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||||
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
|
# pimpl so no consumer target needs this path.
|
||||||
|
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
@@ -538,6 +661,12 @@ if (NOT ANDROID)
|
|||||||
add_subdirectory(MobileGL/MG_Test)
|
add_subdirectory(MobileGL/MG_Test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# After MG_Test so googletest is already available when the unit tests are
|
||||||
|
# built; the module fetches its own copy when they are not.
|
||||||
|
if (MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||||
|
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MOBILEGL_BUILD_BENCHMARK)
|
if (MOBILEGL_BUILD_BENCHMARK)
|
||||||
add_subdirectory(MobileGL/MG_Benchmark)
|
add_subdirectory(MobileGL/MG_Benchmark)
|
||||||
endif()
|
endif()
|
||||||
@@ -546,3 +675,10 @@ if (NOT ANDROID)
|
|||||||
add_subdirectory(tools/trace_replay)
|
add_subdirectory(tools/trace_replay)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The integration binary is also useful as a standalone adb-shell executable.
|
||||||
|
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
||||||
|
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
||||||
|
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||||
|
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||||
|
endif()
|
||||||
|
|||||||
+117
-5
@@ -29,6 +29,33 @@ namespace MobileGL::MG_Config {
|
|||||||
ForceOff,
|
ForceOff,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
|
||||||
|
// never a demand: the renderer clamps it to what the device supports at device
|
||||||
|
// creation, falling down the chain ext -> indirect -> unroll with one log line.
|
||||||
|
enum class MultiDrawMode : Uint8 {
|
||||||
|
Auto = 0, // unset: best supported tier
|
||||||
|
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
|
||||||
|
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
|
||||||
|
Unroll, // one vkCmdDraw* per sub-draw
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
|
||||||
|
// such entry point in core, so every tier below is an emulation; they differ only in
|
||||||
|
// which driver capability they lean on and how many driver calls a batch costs. Like
|
||||||
|
// the Magma knob this is a preference, clamped at resolution time to what the ES
|
||||||
|
// driver actually supports, with one log line when it falls back.
|
||||||
|
enum class GLESMultiDrawMode : Uint8 {
|
||||||
|
Auto = 0, // unset: best supported tier
|
||||||
|
Ext, // one glMultiDrawElementsBaseVertexEXT
|
||||||
|
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
|
||||||
|
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
|
||||||
|
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
|
||||||
|
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
|
||||||
|
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
|
||||||
|
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
|
||||||
|
// drawn by a single glDrawElements
|
||||||
|
};
|
||||||
|
|
||||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||||
@@ -42,14 +69,58 @@ namespace MobileGL::MG_Config {
|
|||||||
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;
|
||||||
|
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||||
|
// Disabled by default because validation is a diagnostics-only cost.
|
||||||
|
Bool EnableSpirvValidation = false;
|
||||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||||
Bool UseAngle = false;
|
Bool UseAngle = false;
|
||||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||||
String TraceAngleVariant;
|
String TraceAngleVariant;
|
||||||
#endif
|
#endif
|
||||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||||
|
// including the opt-in emulated compute path below.
|
||||||
Bool DisableSubgroup = false;
|
Bool DisableSubgroup = false;
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||||
|
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||||
|
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||||
|
// engages when this flag is set AND the device has no native subgroup support at
|
||||||
|
// all - a device with real subgroup operations always uses them natively,
|
||||||
|
// whatever their width (the known iterationRP defect is patched by
|
||||||
|
// FixIterationRPSubgroupScratch below instead). Off by default.
|
||||||
|
Bool MagmaEmulateSubgroup = false;
|
||||||
|
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||||
|
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||||
|
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||||
|
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||||
|
// bounds. The pass grows that one array to what the device's topology needs and
|
||||||
|
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||||
|
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||||
|
// so every other shader passes through byte-identical - as does iterationRP
|
||||||
|
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||||
|
// verbatim.
|
||||||
|
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||||
|
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||||
|
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||||
|
Bool IterationRPFixBarrier = false;
|
||||||
|
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||||
|
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||||
|
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||||
|
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||||
|
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||||
|
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||||
|
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||||
|
// to the raw driver builtin.
|
||||||
|
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||||
|
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||||
|
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||||
|
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
|
||||||
|
// Off by default so an application that checks the string before using doubles keeps
|
||||||
|
// its float path; on for measuring what the conformance suite makes of the demoted
|
||||||
|
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
|
||||||
|
Bool AdvertiseFp64 = false;
|
||||||
// MOBILEGL_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.
|
||||||
@@ -57,6 +128,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
|
||||||
@@ -70,16 +148,19 @@ 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
|
||||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||||
// version request.
|
// version request.
|
||||||
Bool RelaxedSemantics = false;
|
Bool RelaxedSemantics = false;
|
||||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
|
||||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
|
||||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
|
||||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
|
||||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||||
@@ -91,6 +172,37 @@ namespace MobileGL::MG_Config {
|
|||||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||||
Bool DisableRobustBufferAccess = false;
|
Bool DisableRobustBufferAccess = false;
|
||||||
|
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||||
|
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||||
|
// unset picks the best supported tier.
|
||||||
|
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
|
||||||
|
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
|
||||||
|
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
|
||||||
|
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
|
||||||
|
// supported tier, which never includes "compute" - see the note on its resolution.
|
||||||
|
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
|
||||||
|
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
|
||||||
|
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
|
||||||
|
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
|
||||||
|
// both the threading and the application-visible behaviour change.
|
||||||
|
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
||||||
|
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||||
|
Uint32 AsyncShaderCompileThreads = 0;
|
||||||
|
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
|
||||||
|
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
|
||||||
|
// empty, WITHOUT joining the job (latched per compile - see
|
||||||
|
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
|
||||||
|
// a real failure still fails the program link with the compile log quoted. It
|
||||||
|
// exists for applications that compile hundreds of shaders serially and read the
|
||||||
|
// status right after each glCompileShader - Iris's shader-pack load - where those
|
||||||
|
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
|
||||||
|
// phase issues no program-level query between programs; program-level LINK_STATUS
|
||||||
|
// and the program info log still join truthfully, so paths that check each link
|
||||||
|
// immediately stay serial by their own construction). Off by default; never
|
||||||
|
// advertise it.
|
||||||
|
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -97,6 +97,47 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
: MG_Config::QuirkOverride::ForceOff;
|
: MG_Config::QuirkOverride::ForceOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
|
||||||
|
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
|
||||||
|
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
|
||||||
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
|
return MG_Config::MultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
String lowered = it->second;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
|
||||||
|
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
|
||||||
|
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
|
||||||
|
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
|
||||||
|
key.c_str(), it->second.c_str());
|
||||||
|
return MG_Config::MultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
|
||||||
|
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
|
||||||
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
|
return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
String lowered = it->second;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
|
||||||
|
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
|
||||||
|
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
|
||||||
|
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
|
||||||
|
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
|
||||||
|
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
|
||||||
|
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
|
||||||
|
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
|
||||||
|
key.c_str(), it->second.c_str());
|
||||||
|
return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
|
||||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||||
auto it = acceptedEnvVariablesMap->find(key);
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
if (it == acceptedEnvVariablesMap->end()) {
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
@@ -121,23 +162,38 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
inline void InitFeatures() {
|
inline void InitFeatures() {
|
||||||
auto& features = MG_Config::Features;
|
auto& features = MG_Config::Features;
|
||||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||||
|
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||||
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.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||||
|
features.FixIterationRPSubgroupScratch =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
|
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
||||||
|
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
||||||
|
features.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.MagmaDisableBlendedDepthWriteQuirk =
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
|
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||||
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
|
features.AsyncOptimisticShaderStatus =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
+37
-1
@@ -15,6 +15,9 @@
|
|||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -37,18 +40,38 @@ namespace MobileGL {
|
|||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
MGLOG_I("MobileGL closing...");
|
MGLOG_I("MobileGL closing...");
|
||||||
}
|
}
|
||||||
glslang::FinalizeProcess();
|
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||||
|
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||||
|
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||||
|
// both of which this function is about to destroy. This is the one
|
||||||
|
// cancellation path in the whole design that waits.
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
||||||
// GL syncs die with their contexts, and every context is gone by the
|
// GL syncs die with their contexts, and every context is gone by the
|
||||||
// time full teardown runs: drain the live-sync registry while the
|
// time full teardown runs: drain the live-sync registry while the
|
||||||
// backend function table can still release the backend handles (and
|
// backend function table can still release the backend handles (and
|
||||||
// before a re-initialized library could pair them with the wrong
|
// before a re-initialized library could pair them with the wrong
|
||||||
// backend's DeleteSync).
|
// backend's DeleteSync).
|
||||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
|
// Queries die with their contexts for the same reason, and their registry
|
||||||
|
// is the same shape of process-global map: drain it here too, while the
|
||||||
|
// function table can still pair each backend handle with the backend that
|
||||||
|
// minted it.
|
||||||
|
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
|
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
|
||||||
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
|
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
|
||||||
|
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
|
||||||
|
// glslang's process-wide pool allocator and every cached built-in symbol table,
|
||||||
|
// while the TShader/TProgram objects owned by the shader and program objects
|
||||||
|
// still reference levels adopted from those tables. Finalizing first left live
|
||||||
|
// glslang objects pointing at freed memory for the rest of the teardown.
|
||||||
|
glslang::FinalizeProcess();
|
||||||
|
// Immediately after, and never apart from it: FinalizeProcess just deleted the
|
||||||
|
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||||
|
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||||
MG_Backend::gBackendFunctionsTable = {};
|
MG_Backend::gBackendFunctionsTable = {};
|
||||||
g_isInitialized = false;
|
g_isInitialized = false;
|
||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
@@ -76,6 +99,19 @@ namespace MobileGL {
|
|||||||
MG_Impl::Init();
|
MG_Impl::Init();
|
||||||
MGLOG_D("MG_Impl initialized");
|
MGLOG_D("MG_Impl initialized");
|
||||||
glslang::InitializeProcess();
|
glslang::InitializeProcess();
|
||||||
|
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
|
||||||
|
// tables lazily under a process-wide lock held for the whole build, so without this
|
||||||
|
// the first concurrent compiles of a shaderpack all serialize behind the very first
|
||||||
|
// parse and asynchronous compilation looks like it is doing nothing.
|
||||||
|
//
|
||||||
|
// Gated on the flag, because the problem it solves only exists when there are
|
||||||
|
// workers: with compilation synchronous, nothing ever contends for that lock and the
|
||||||
|
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
|
||||||
|
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
|
||||||
|
// on the async subsystem (ProgramUtilTest compiles that file without it).
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
|
||||||
|
}
|
||||||
MGLOG_D("glslang initialized");
|
MGLOG_D("glslang initialized");
|
||||||
g_isInitialized = true;
|
g_isInitialized = true;
|
||||||
MGLOG_I("MobileGL initialized");
|
MGLOG_I("MobileGL initialized");
|
||||||
|
|||||||
@@ -181,15 +181,18 @@ namespace MobileGL {
|
|||||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||||
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||||
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
// a backend query: it describes the program the application wrote, in the
|
||||||
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
// application's namespace, which neither backend program is in. It is answered
|
||||||
GLsizei* length, GLchar* name);
|
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
|
||||||
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
// the application passes is the frontend interface-query enumeration's, and no
|
||||||
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
|
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
|
||||||
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
|
||||||
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
// The name is the one coordinate all three agree on, so the frontend resolves the
|
||||||
|
// index against its own enumeration and each backend maps the name to its own.
|
||||||
|
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
|
||||||
|
GLuint storageBlockBinding);
|
||||||
// GL fence sync objects. All entries are optional (may be null); the
|
// GL fence sync objects. All entries are optional (may be null); the
|
||||||
// frontend then falls back to always-signaled sync semantics.
|
// frontend then falls back to always-signaled sync semantics.
|
||||||
// FenceSync may itself return null when the backend cannot create a
|
// FenceSync may itself return null when the backend cannot create a
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -209,7 +210,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
||||||
}
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
}
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
reasons.push_back("EXT_render_snorm not supported");
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
@@ -552,26 +553,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||||
// A multisample texture can only ever be rendered into, so its storage format
|
// Colour-attachable targets need a colour-renderable fallback; the ordinary
|
||||||
// has to stay colour-renderable; the ordinary fallback for a three-channel
|
// fallback for a three-channel format is another three-channel one, which ES
|
||||||
// format is a three-channel one, which ES accepts as a texture but rejects as
|
// accepts as a texture but never as an attachment. Recompute the fallback per
|
||||||
// multisample storage. Recompute the fallback per target so those formats get
|
// target so those formats get widened where the target demands it.
|
||||||
// widened here and nowhere else.
|
const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
|
||||||
Flags<PixelFormatNormalizeOptionBit> targetOptions;
|
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
// Multisample storage has no three-channel form on ES at all, so its widening
|
||||||
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
// is unconditional and skips the native probe (which cannot succeed). Every
|
||||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
// other target keeps the widening on the DRIVER branch, behind the native
|
||||||
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
// probe: `shouldProbeFallback = !nativeCreated || !nativeRenderable` below is
|
||||||
}
|
// what makes the substitution conditional on the driver actually refusing, so
|
||||||
}
|
// a driver that does render to a three-channel image keeps allocating it byte
|
||||||
|
// for byte. That is a per-format runtime answer, NOT a desktop-vs-device
|
||||||
|
// split: llvmpipe renders to GL_RGB16F but refuses GL_RGB8_SNORM, GL_SRGB8,
|
||||||
|
// GL_RGB32F and the RGB integer formats, so the CI driver widens those eight
|
||||||
|
// too. Re-run the retrace fixtures and the glcts suites on any change here.
|
||||||
|
const Bool widenUnconditionally = IsGLESProbeMultisampleTarget(target);
|
||||||
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
||||||
Bool hasForcedFallback = outerHasForcedFallback;
|
Bool hasForcedFallback = outerHasForcedFallback;
|
||||||
if (targetOptions) {
|
if (renderTargetOptions) {
|
||||||
hasForcedFallback = BuildFallbackProbeFormatInfo(
|
// Folded into the forced options only when a forced fallback already
|
||||||
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
|
// applies, so the render-target bits never *create* one: ANGLE's forced
|
||||||
if (!hasForcedFallback) {
|
// GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, false,
|
// widening, but a non-ANGLE driver must not lose its native probe.
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
|
||||||
|
(outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions
|
||||||
|
: forcedOptions;
|
||||||
|
hasForcedFallback =
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true,
|
||||||
fallbackInfo);
|
fallbackInfo);
|
||||||
|
if (!hasForcedFallback) {
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat,
|
||||||
|
driverOptions | renderTargetOptions, false, fallbackInfo);
|
||||||
|
}
|
||||||
|
// HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built
|
||||||
|
// for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any
|
||||||
|
// other renderer), and it SKIPS the native probe entirely - the widened
|
||||||
|
// format is asserted rather than measured on this device. That assertion
|
||||||
|
// is validated on exactly one configuration, the android-angle retrace
|
||||||
|
// golden; it is NOT covered by the headless llvmpipe suites, which take
|
||||||
|
// the driver branch below and prove nothing about ANGLE's answers. So log
|
||||||
|
// the choice at INFO rather than the usual MGLOG_D caveat: on any other
|
||||||
|
// ANGLE device the device report is the only evidence there is of which
|
||||||
|
// storage format the image really got. Once per format on the ordinary 2D
|
||||||
|
// target - repeating it for all ten targets would bury the report.
|
||||||
|
if (hasForcedFallback && target == TextureTarget::Texture2D &&
|
||||||
|
(MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(
|
||||||
|
requestedInternalFormat, renderTargetOptions) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) {
|
||||||
|
MGLOG_I("Three-channel widening (FORCED path, no native probe): %s stored as %s. "
|
||||||
|
"Reason: %s. Device-validated on the android-angle golden only.",
|
||||||
|
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
||||||
|
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str(),
|
||||||
|
fallbackInfo.Reason.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,8 +652,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||||
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
// A renderbuffer exists only to be attached, so it needs the same three-channel
|
||||||
if (!outerHasForcedFallback) {
|
// widening the colour-attachable texture targets get - and on the same terms: the
|
||||||
|
// native storage is probed first, so a driver that renders to it keeps it.
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> renderbufferOptions =
|
||||||
|
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex);
|
||||||
|
GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo;
|
||||||
|
Bool renderbufferHasForcedFallback = outerHasForcedFallback;
|
||||||
|
if (renderbufferOptions) {
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
|
||||||
|
outerHasForcedFallback ? forcedOptions | renderbufferOptions : forcedOptions;
|
||||||
|
renderbufferHasForcedFallback = BuildFallbackProbeFormatInfo(
|
||||||
|
requestedInternalFormat, forcedProbeOptions, true, renderbufferFallbackInfo);
|
||||||
|
if (!renderbufferHasForcedFallback) {
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderbufferOptions,
|
||||||
|
false, renderbufferFallbackInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool shouldProbeFallbackRenderbuffer = renderbufferHasForcedFallback;
|
||||||
|
if (!renderbufferHasForcedFallback) {
|
||||||
const Bool nativeRenderbufferComplete =
|
const Bool nativeRenderbufferComplete =
|
||||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||||
if (nativeRenderbufferComplete) {
|
if (nativeRenderbufferComplete) {
|
||||||
@@ -632,16 +685,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
shouldProbeFallbackRenderbuffer = true;
|
shouldProbeFallbackRenderbuffer = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||||
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||||
}
|
}
|
||||||
const Int maxSamples =
|
const Int maxSamples =
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||||
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
{
|
{
|
||||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||||
},
|
},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||||
@@ -681,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// thread can only observe the extension string after the
|
// thread can only observe the extension string after the
|
||||||
// advertisement for its context has settled; rebuilding the whole
|
// advertisement for its context has settled; rebuilding the whole
|
||||||
// list keeps the re-run after a context recreation idempotent.
|
// list keeps the re-run after a context recreation idempotent.
|
||||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||||
|
capabilities.SupportsDrawIndirect,
|
||||||
|
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -726,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
// reconcile every runtime-gated advertisement (see the comment on
|
||||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||||
// list is first built).
|
// built).
|
||||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -871,11 +926,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return MutableRendererInfo();
|
return MutableRendererInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||||
|
Bool drawIndirectSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||||
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
||||||
@@ -887,10 +944,59 @@ 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.
|
||||||
E_GL_ARB_get_program_binary};
|
E_GL_ARB_get_program_binary};
|
||||||
|
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||||
|
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||||
|
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||||
|
if (drawIndirectSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||||
|
}
|
||||||
|
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||||
|
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||||
|
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||||
|
// that incomplete case.
|
||||||
|
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_base_instance);
|
||||||
|
}
|
||||||
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||||
|
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||||
|
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||||
|
// the string is irrelevant here (the POST reports it separately, for the day the
|
||||||
|
// driver-side link is what gets parallelised).
|
||||||
|
//
|
||||||
|
// Gated on the async flag deliberately, and this is the whole reason the gate
|
||||||
|
// exists. Advertising the string is the one part of asynchronous compilation that a
|
||||||
|
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
|
||||||
|
// the moment they see it - they enqueue whole pipeline batches and poll
|
||||||
|
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
|
||||||
|
// change as well as the threading, or the kill switch would only be half a switch.
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
|
}
|
||||||
|
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). 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.
|
||||||
@@ -933,6 +1039,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;
|
||||||
@@ -951,12 +1058,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
|
||||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
|
||||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
|
||||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
|
||||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||||
funcsTable.GL.Clear = Clear;
|
funcsTable.GL.Clear = Clear;
|
||||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||||
@@ -1088,6 +1189,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;
|
||||||
|
|||||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const RendererInfo& GetRendererIdentity();
|
const RendererInfo& GetRendererIdentity();
|
||||||
|
|
||||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||||
|
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||||
|
Bool drawIndirectSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported);
|
||||||
|
|
||||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
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);
|
||||||
@@ -92,15 +94,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
|
||||||
GLchar* name);
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
|
||||||
Bool InitWindowSurface(NativeWindowType window);
|
Bool InitWindowSurface(NativeWindowType window);
|
||||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||||
Bool MakeCurrent();
|
Bool MakeCurrent();
|
||||||
@@ -125,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
|
||||||
@@ -182,6 +194,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace XfbImpl {
|
namespace XfbImpl {
|
||||||
Bool AreTransformFeedbacksSupported();
|
Bool AreTransformFeedbacksSupported();
|
||||||
|
// True while a capture span is open on the current transform feedback object
|
||||||
|
// (frontend Begin seen and not paused), whether or not the deferred driver-side
|
||||||
|
// Begin has been issued yet. Draw paths that would restructure the primitive
|
||||||
|
// stream, or that need to dispatch compute mid-draw, decline while it is set.
|
||||||
|
Bool IsCaptureSpanOpen();
|
||||||
void BeginTransformFeedback(GLenum primitiveMode);
|
void BeginTransformFeedback(GLenum primitiveMode);
|
||||||
void EndTransformFeedback();
|
void EndTransformFeedback();
|
||||||
void PauseTransformFeedback();
|
void PauseTransformFeedback();
|
||||||
@@ -191,6 +208,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void OnBackendContextDestroyed();
|
void OnBackendContextDestroyed();
|
||||||
} // namespace XfbImpl
|
} // namespace XfbImpl
|
||||||
|
|
||||||
|
namespace RenderStateImpl {
|
||||||
|
// Pushes the frontend's render-state block to the ES driver, diffed against what was
|
||||||
|
// last pushed.
|
||||||
|
//
|
||||||
|
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
|
||||||
|
// mask handed to the driver. A draw into a colour attachment the backend widened from
|
||||||
|
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
|
||||||
|
// move the stored alpha away from the 1.0 the application's three-channel format
|
||||||
|
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
|
||||||
|
// gets there in the first place, so it must be allowed to write alpha - hence the flag
|
||||||
|
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
|
||||||
|
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
|
||||||
|
// frontend version.
|
||||||
|
//
|
||||||
|
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
|
||||||
|
// answers from the frontend state, which this function only reads.
|
||||||
|
void SyncRenderState(Bool forColorClear = false);
|
||||||
|
void InvalidateSyncedRenderState();
|
||||||
|
} // namespace RenderStateImpl
|
||||||
|
|
||||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
||||||
|
|
||||||
|
// True once the process has entered exit(): past that point the EGL library and
|
||||||
|
// the driver may already be unloaded, so a backend twin's destructor must not
|
||||||
|
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
||||||
|
// pointer from __run_exit_handlers) nor touch statics in other TUs (cross-TU
|
||||||
|
// destruction order is unspecified). Deliberate leak: the process is exiting and
|
||||||
|
// the driver reclaims GPU objects. The flag is set by a std::atexit handler that
|
||||||
|
// EnsureProcessTeardownSentinel() registers lazily on first registry use - by
|
||||||
|
// then every static everywhere has finished constructing, so this handler is
|
||||||
|
// guaranteed to run BEFORE any static destructor (atexit is LIFO). A destructor
|
||||||
|
// hook on the registry itself was tried first and is WRONG: tests and cache
|
||||||
|
// resets destroy temporary registry instances mid-run, which would latch the
|
||||||
|
// flag while the process is very much alive.
|
||||||
|
Bool InProcessTeardown();
|
||||||
|
void EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
// Generation of the backend ES context that owns the driver ids currently handed
|
||||||
|
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
||||||
|
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
|
||||||
|
// construction and compares it in its destructor: a twin outliving its context
|
||||||
|
// must NOT glDelete* its id, because a successor context may already have recycled
|
||||||
|
// that name and the delete would take out a live object of the new context.
|
||||||
|
extern Uint g_backendContextGeneration;
|
||||||
|
|
||||||
|
// Which optional pieces of state a draw needs synchronized before it is issued.
|
||||||
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||||
|
// draws that provably cannot read them.
|
||||||
|
enum class DrawSyncBit : Uint32 {
|
||||||
|
None = 0,
|
||||||
|
IndexBuffer = 1 << 0,
|
||||||
|
IndirectBuffer = 1 << 1,
|
||||||
|
Instancing = 1 << 2
|
||||||
|
};
|
||||||
|
// Deliberately the shared Flags<> rather than hand-written operators for this enum:
|
||||||
|
// a namespace-local operator| here would hide MobileGL::operator|(Bit, Bit) from
|
||||||
|
// every other scoped-enum flag set used inside this namespace.
|
||||||
|
using DrawSyncFlags = Flags<DrawSyncBit>;
|
||||||
|
|
||||||
|
// The GL-defined indirect command layouts, byte-identical to what the driver reads
|
||||||
|
// out of a GL_DRAW_INDIRECT_BUFFER. Also the staging layout the multi-draw emulation
|
||||||
|
// synthesizes commands into.
|
||||||
|
struct DrawElementsIndirectCommand {
|
||||||
|
Uint32 count = 0;
|
||||||
|
Uint32 instanceCount = 0;
|
||||||
|
Uint32 firstIndex = 0;
|
||||||
|
Int32 baseVertex = 0;
|
||||||
|
Uint32 baseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DrawArraysIndirectCommand {
|
||||||
|
Uint32 count = 0;
|
||||||
|
Uint32 instanceCount = 0;
|
||||||
|
Uint32 first = 0;
|
||||||
|
Uint32 baseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||||
|
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||||
|
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||||
|
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||||
|
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||||
|
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||||
|
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||||
|
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||||
|
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||||
|
void SetCurrentDrawID(Uint32 drawId);
|
||||||
|
// GL's gl_BaseVertex is the base-vertex parameter of an indexed draw and zero for every
|
||||||
|
// command that has none - including all the DrawArrays forms - so every draw path that
|
||||||
|
// does not carry one must leave this at zero rather than inherit the last draw's value.
|
||||||
|
void SetCurrentBaseVertex(Int32 baseVertex);
|
||||||
|
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
||||||
|
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||||
|
// batch and would therefore be wrong.
|
||||||
|
Bool CurrentProgramReadsDrawID();
|
||||||
|
// Same question for gl_BaseVertex: a batched multi-draw tier cannot give each sub-draw
|
||||||
|
// its own base vertex through a uniform either.
|
||||||
|
Bool CurrentProgramReadsBaseVertex();
|
||||||
|
// Both of the above, conservatively, for a caller that must decide BEFORE PrepareForDraw
|
||||||
|
// has synced the program - where "does not read it" is indistinguishable from "cannot be
|
||||||
|
// asked yet". Answers true whenever the backend twin is missing or predates the current
|
||||||
|
// link.
|
||||||
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||||
|
|
||||||
template <typename StateObject, typename BackendObject>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
class StateBackendObjectRegistry {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
using StatePtr = SharedPtr<StateObject>;
|
using StatePtr = SharedPtr<StateObject>;
|
||||||
using StateWeakPtr = std::weak_ptr<StateObject>;
|
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||||
using BackendPtr = SharedPtr<BackendObject>;
|
using BackendPtr = SharedPtr<BackendObject>;
|
||||||
@@ -43,6 +126,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||||
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
|
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
|
||||||
|
|
||||||
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||||
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
||||||
|
EnsureProcessTeardownSentinel();
|
||||||
auto& entry = m_entries[stateObj.get()];
|
auto& entry = m_entries[stateObj.get()];
|
||||||
if (entry.stateRef.expired()) {
|
if (entry.stateRef.expired()) {
|
||||||
// The previous owner of this address is gone and the allocator handed it
|
// The previous owner of this address is gone and the allocator handed it
|
||||||
@@ -55,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()) {
|
||||||
@@ -195,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
|
||||||
@@ -247,6 +346,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||||
// could false-skip when the name is recycled.
|
// could false-skip when the name is recycled.
|
||||||
void NoteBufferIdDeleted(Uint id);
|
void NoteBufferIdDeleted(Uint id);
|
||||||
|
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
|
||||||
|
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
|
||||||
|
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
|
||||||
|
// key on FRONTEND versions, which a backend-side re-mint does not move - without
|
||||||
|
// this generation the driver VAO would keep fetching through the deleted id (or
|
||||||
|
// its retained store) forever. Compared and stamped by
|
||||||
|
// BackendVertexArrayObject::SyncToBackend.
|
||||||
|
extern Uint64 g_bufferBackendIdGeneration;
|
||||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||||
@@ -366,6 +473,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
|
// Identity of the buffer the version above was stamped against. Raw and never
|
||||||
|
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
|
||||||
|
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
|
||||||
|
// alone would read a wrapped-back count with a different buffer bound as clean.
|
||||||
|
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
|
||||||
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
||||||
// version on every per-attribute version bump (the three Bump*Version functions are
|
// version on every per-attribute version bump (the three Bump*Version functions are
|
||||||
// its only writers), so an unchanged config version proves every per-attribute
|
// its only writers), so an unchanged config version proves every per-attribute
|
||||||
@@ -375,6 +487,17 @@ 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;
|
||||||
|
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
|
||||||
|
// mismatch means some live buffer's driver id was re-minted since; the ids
|
||||||
|
// baked into the driver VAO's attribute/element bindings may be dead even
|
||||||
|
// though every frontend version matches, so the next sync re-emits them all.
|
||||||
|
Uint64 m_syncedBufferIdGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||||
@@ -388,6 +511,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 {
|
||||||
@@ -449,6 +589,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
|
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Components per texel the frontend format's client data carries, for the three-channel
|
||||||
|
// formats that can be widened to a four-channel colour-renderable target; 0 for everything
|
||||||
|
// else. See PrepareChannelWidenedUpload.
|
||||||
|
Uint GetWidenableClientComponentCount(TextureInternalFormat format);
|
||||||
|
|
||||||
|
// True when a widenable format's components are integer rather than normalized, which is
|
||||||
|
// what decides the synthetic alpha's value: GL_RGB8I and GL_RGB8_SNORM are both uploaded
|
||||||
|
// as GL_BYTE, but their 1.0 is 1 and 0x7F respectively.
|
||||||
|
Bool IsIntegerWidenableFormat(TextureInternalFormat format);
|
||||||
|
|
||||||
|
// Repacks three-component client data as four components with an alpha of 1.0 in
|
||||||
|
// `uploadType`, for a format the backend widened to keep a colour attachment renderable.
|
||||||
|
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
||||||
|
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
||||||
|
// scratch buffer and has to outlive the returned pointer.
|
||||||
|
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||||
|
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||||
|
Bool integerData = false);
|
||||||
|
|
||||||
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
SizeT width = 0;
|
SizeT width = 0;
|
||||||
@@ -528,6 +687,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
|
||||||
@@ -553,8 +717,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);
|
||||||
@@ -564,19 +738,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
Bool imageBindableStorageRequired = false);
|
Bool imageBindableStorageRequired = false);
|
||||||
|
// Brings every texture the next draw reads - the touched units' bindings and the draw
|
||||||
|
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
||||||
|
// documented at their definitions. Declared here so tests can drive those memos directly.
|
||||||
|
void SyncNeccessaryTextures();
|
||||||
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||||
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
|
||||||
@@ -591,6 +774,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
|
||||||
@@ -633,6 +817,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||||
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
||||||
|
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
|
||||||
|
// mismatch means some backend texture id was re-minted since, and any of this
|
||||||
|
// twin's attachment points may still hold the dead id even though the frontend
|
||||||
|
// attachment versions match - so the walk re-attaches everything first.
|
||||||
|
Uint64 m_syncedBackendIdGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||||
@@ -644,6 +833,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// has to apply the clamp itself.
|
// has to apply the clamp itself.
|
||||||
Bool IsFixedPointFallbackReadAttachment();
|
Bool IsFixedPointFallbackReadAttachment();
|
||||||
|
|
||||||
|
// True when the read buffer names a three-channel attachment the backend actually stores
|
||||||
|
// in a four-channel format (the colour-renderable widening). A format without alpha reads
|
||||||
|
// back as 1.0, so the readback path has to overwrite the alpha the draw left behind -
|
||||||
|
// unconditionally, since this is the format's own semantics rather than the
|
||||||
|
// GL_CLAMP_READ_COLOR rule the clamp above implements.
|
||||||
|
Bool IsAlphaWidenedFallbackReadAttachment();
|
||||||
|
|
||||||
|
// True when this attachment's storage carries an alpha channel its frontend format does
|
||||||
|
// not (the three-channel colour-renderable widening).
|
||||||
|
Bool IsAlphaWidenedColorAttachment(const MG_State::GLState::FramebufferAttachmentObject& attachmentObject);
|
||||||
|
|
||||||
|
// Bit i set = DRAW BUFFER i of `fbo` resolves to a colour attachment the backend widened
|
||||||
|
// from three channels to four. Indexed by draw-buffer slot, not by attachment point,
|
||||||
|
// because that is what glColorMaski / glClearBufferfv address.
|
||||||
|
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo);
|
||||||
|
|
||||||
|
// The same mask for whatever is currently bound to GL_DRAW_FRAMEBUFFER, recomputed by
|
||||||
|
// SyncCurrentFBO (BackendFramebufferObject::SyncToBackend for the DRAW target, and reset
|
||||||
|
// to 0 on the default framebuffer). Read by the draw/clear state sync, so it is only
|
||||||
|
// trustworthy after SyncCurrentFBO has run in the same entry point.
|
||||||
|
//
|
||||||
|
// WHY IT EXISTS (the dst-alpha discipline). A widened attachment has a real alpha channel
|
||||||
|
// the application's format does not, and GL says a missing channel reads as 1.0. Readback
|
||||||
|
// can paper over that (ForceWideReadAlphaToOne), but GL_DST_ALPHA /
|
||||||
|
// GL_ONE_MINUS_DST_ALPHA blending and glBlitFramebuffer read the STORED alpha inside the
|
||||||
|
// driver where no interception is possible. So the stored alpha is kept at 1.0 instead:
|
||||||
|
// a clear touching a widened buffer writes alpha 1.0, and every draw into it has its
|
||||||
|
// alpha write mask forced off, so nothing can ever move it again. The application's own
|
||||||
|
// colour mask is untouched - glGet(GL_COLOR_WRITEMASK) still reports what it set.
|
||||||
|
extern Uint32 g_alphaWidenedDrawBufferMask;
|
||||||
|
|
||||||
|
// Bit i set = DRAW BUFFER i of the framebuffer bound as DRAW resolves to a colour
|
||||||
|
// attachment with an INTEGER format. Recomputed beside the mask above and for its sake:
|
||||||
|
// glClearBufferfv on an integer colour buffer is GL_INVALID_OPERATION, so the
|
||||||
|
// per-draw-buffer clear route the widening needs has to stand down when one is present.
|
||||||
|
// (glClear on an integer colour buffer is left undefined by ES in the first place, and
|
||||||
|
// an application that wants a defined answer has to call glClearBufferuiv/iv - which does
|
||||||
|
// carry the widened alpha substitution.)
|
||||||
|
extern Uint32 g_integerColorDrawBufferMask;
|
||||||
|
|
||||||
|
// The colour a clear has to hand the driver for one draw buffer: the application's value,
|
||||||
|
// except that a widened attachment's alpha is replaced by the 1.0 its three-channel
|
||||||
|
// format implies. `one` is 1.0 encoded in the clear call's own component type - the
|
||||||
|
// integer clears carry the integer 1, the float clear carries 1.0f.
|
||||||
|
//
|
||||||
|
// Returns `value` itself when nothing is substituted, so the ordinary path allocates and
|
||||||
|
// copies nothing; `scratch` is the caller's buffer and has to outlive the returned
|
||||||
|
// pointer. Free of GL state on purpose, so the substitution can be unit-tested exactly as
|
||||||
|
// the driver sees it.
|
||||||
|
template <typename T>
|
||||||
|
const T* SubstituteWidenedClearAlpha(const T* value, Bool widened, T one, T (&scratch)[4]) {
|
||||||
|
if (!widened || value == nullptr) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
scratch[0] = value[0];
|
||||||
|
scratch[1] = value[1];
|
||||||
|
scratch[2] = value[2];
|
||||||
|
scratch[3] = one;
|
||||||
|
return scratch;
|
||||||
|
}
|
||||||
|
|
||||||
// What SyncCurrentFBO last pushed for each target, as a (binding, object, revision)
|
// What SyncCurrentFBO last pushed for each target, as a (binding, object, revision)
|
||||||
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
||||||
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
||||||
@@ -661,6 +911,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||||
g_fboSyncedObjects;
|
g_fboSyncedObjects;
|
||||||
|
|
||||||
|
// Bumped whenever a live backend texture's driver id is re-minted while its
|
||||||
|
// frontend texture may still be attached to application FBOs
|
||||||
|
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
|
||||||
|
// whose backend storage went immutable). The FBO twins' attachment memos key on
|
||||||
|
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
|
||||||
|
// the driver FBO would keep the deleted texture name attached forever. The
|
||||||
|
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
||||||
|
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
||||||
|
extern Uint64 g_attachmentBackendIdGeneration;
|
||||||
|
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
||||||
|
// target; part of the synced tuple above.
|
||||||
|
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
|
||||||
|
|
||||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||||
@@ -671,6 +934,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
|
||||||
@@ -847,6 +1114,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
|
||||||
|
// actually reaches a shader read rather than being discarded.
|
||||||
|
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 0; }
|
||||||
|
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||||
|
// uniform write, and only such a program needs the reset after one.
|
||||||
|
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
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
|
||||||
@@ -857,6 +1131,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; }
|
||||||
@@ -872,14 +1151,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;
|
||||||
@@ -887,6 +1200,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;
|
||||||
|
|
||||||
@@ -897,6 +1212,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -912,18 +1234,87 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern Uint g_lastUsedBackendProgramId;
|
extern Uint g_lastUsedBackendProgramId;
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||||
g_backendProgramObjects;
|
g_backendProgramObjects;
|
||||||
|
|
||||||
|
// Points one shader storage block of an ALREADY-LINKED backend program at
|
||||||
|
// `binding`. `blockName` is the frontend interface-query spelling; the real
|
||||||
|
// driver's own index for it is looked up here, because the transpiled ESSL's
|
||||||
|
// block order is not the frontend's. Returns false when the block does not exist
|
||||||
|
// on the backend program (eliminated as unused, or the driver lacks the entry
|
||||||
|
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||||
|
// record either way.
|
||||||
|
//
|
||||||
|
// NOT how a rebinding reaches the shader. glShaderStorageBlockBinding has no ES
|
||||||
|
// equivalent and is absent from every real ES driver, so this is a no-op there;
|
||||||
|
// SyncToBackend bakes the effective binding into the ESSL it generates instead
|
||||||
|
// (SpvcSession::SetShaderStorageBlockBinding). This is kept as the cheaper path on
|
||||||
|
// a driver that does happen to expose the entry point.
|
||||||
|
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||||
|
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||||
|
// program that was just built - best effort, on the same "only where the driver has
|
||||||
|
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||||
|
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||||
|
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
|
// Order-independent digest of the program's glShaderStorageBlockBinding overrides.
|
||||||
|
// The generated ESSL carries them (ES has no way to move a storage block's binding
|
||||||
|
// after link), so a program built against a different set is stale and the draw path
|
||||||
|
// has to rebuild it. Computed from the values, so re-setting a block to the binding it
|
||||||
|
// already has costs nothing. 0 when nothing was ever rebound.
|
||||||
|
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
|
|
||||||
|
// Everything the image-format bake needs from one walk of a program's uniform
|
||||||
|
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||||
|
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
||||||
|
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||||
|
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
||||||
|
// on it.
|
||||||
|
struct ImageFormatBakeInputs {
|
||||||
|
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
||||||
|
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
||||||
|
// one is authoritative and is never overridden.
|
||||||
|
UnorderedMap<String, Uint> glFormatByUniformName;
|
||||||
|
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
||||||
|
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
||||||
|
// write into the emitted declaration instead. Disjoint from the map above by
|
||||||
|
// construction: a format is baked into the module or completed in the text, never
|
||||||
|
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
||||||
|
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
||||||
|
// Units those uniforms address, kept so the draw path can re-read their formats
|
||||||
|
// without walking the reflection again.
|
||||||
|
Vector<Int> units;
|
||||||
|
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
||||||
|
// image uniform, which is all but a handful.
|
||||||
|
Uint64 signature = 0;
|
||||||
|
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
||||||
|
// declaration carries one qualifier, so there is nothing correct to bake and they
|
||||||
|
// are dropped from the map above. Kept for diagnostics.
|
||||||
|
Vector<String> conflictedNames;
|
||||||
|
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||||
|
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||||
|
Bool needsExtendedImageFormats = false;
|
||||||
|
};
|
||||||
|
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
} // namespace PrgramImpl
|
} // namespace 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;
|
||||||
@@ -941,12 +1332,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;
|
||||||
|
|||||||
@@ -0,0 +1,928 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "MultiDraw.h"
|
||||||
|
#include "Managers.h"
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||||
|
using MG_Config::GLESMultiDrawMode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Batch shape
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SizeT IndexTypeSize(GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE: return 1;
|
||||||
|
case GL_UNSIGNED_SHORT: return 2;
|
||||||
|
case GL_UNSIGNED_INT: return 4;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The all-ones value of an index type, which is what GL restarts on once
|
||||||
|
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||||
|
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||||
|
// restart always restarts here and nowhere else.
|
||||||
|
Uint32 RestartSentinelFor(GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||||
|
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||||
|
default: return 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool RestartActive() {
|
||||||
|
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||||
|
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||||
|
// single draw without changing the primitive stream. Zero for strip/loop/fan modes
|
||||||
|
// (concatenation would weld one sub-draw's last primitive to the next sub-draw's
|
||||||
|
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
|
||||||
|
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case GL_POINTS: return 1;
|
||||||
|
case GL_LINES: return 2;
|
||||||
|
case GL_TRIANGLES: return 3;
|
||||||
|
case GL_LINES_ADJACENCY: return 4;
|
||||||
|
case GL_TRIANGLES_ADJACENCY: return 6;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beyond this an emulated batch would ask for a scratch allocation measured in
|
||||||
|
// hundreds of megabytes (and the scratch ring never shrinks again); decline and let
|
||||||
|
// a per-sub-draw tier handle it instead of trying and failing inside the driver.
|
||||||
|
constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24;
|
||||||
|
|
||||||
|
// The flattening dispatch is one invocation per output index. ES 3.1 only
|
||||||
|
// guarantees 65535 work groups per dimension, and exceeding it makes
|
||||||
|
// glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an
|
||||||
|
// uninitialised index buffer rather than failing visibly. Cap the tier there
|
||||||
|
// instead of querying: 4.19M indices is far past any real multi-draw batch, and
|
||||||
|
// beyond it the per-sub-draw tiers are the better answer anyway.
|
||||||
|
constexpr SizeT kComputeWorkGroupSize = 64;
|
||||||
|
constexpr SizeT kMaxComputeWorkGroups = 65535;
|
||||||
|
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
|
||||||
|
|
||||||
|
Uint BoundDrawIndirectBufferId() {
|
||||||
|
const auto& indirect =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
if (!indirect) return 0;
|
||||||
|
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||||
|
return resource ? resource->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||||
|
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (!vao) return none;
|
||||||
|
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
|
||||||
|
// that swaps in a scratch index buffer has to put back. Restoring the exact name
|
||||||
|
// matters beyond tidiness: the VAO twin memoises that it already synced this
|
||||||
|
// index binding and will not re-issue it on the next draw.
|
||||||
|
Uint BoundIndexBufferId() {
|
||||||
|
const auto& ibo = BoundIndexBuffer();
|
||||||
|
if (!ibo) return 0;
|
||||||
|
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||||
|
return resource ? resource->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scratch GL objects
|
||||||
|
//
|
||||||
|
// All of them belong to the ES context and are abandoned (not deleted) when it
|
||||||
|
// dies, exactly like XfbImpl's scatter buffer: the names are the dead context's
|
||||||
|
// to reclaim, and deleting them would target whatever the successor context
|
||||||
|
// handed out for the same name.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct ScratchBuffer {
|
||||||
|
Uint id = 0;
|
||||||
|
SizeT capacity = 0;
|
||||||
|
SizeT cursor = 0; // ring buffers only: next free byte
|
||||||
|
};
|
||||||
|
|
||||||
|
ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array
|
||||||
|
ScratchBuffer g_rebasedIndices; // CPU-rebased index stream
|
||||||
|
ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors
|
||||||
|
ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream
|
||||||
|
|
||||||
|
Uint g_computeProgram = 0;
|
||||||
|
Bool g_computeProgramFailed = false;
|
||||||
|
GLint g_uElementSize = -1;
|
||||||
|
GLint g_uDrawCount = -1;
|
||||||
|
GLint g_uTotalIndices = -1;
|
||||||
|
|
||||||
|
// Reused staging, so a steady stream of batches allocates nothing.
|
||||||
|
Vector<DrawElementsIndirectCommand> g_commandStaging;
|
||||||
|
Vector<Uint32> g_indexStaging;
|
||||||
|
Vector<Uint32> g_drawInfoStaging;
|
||||||
|
Vector<GLint> g_zeroBaseVertices;
|
||||||
|
|
||||||
|
// Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target
|
||||||
|
// (BufferImpl::TempBufferTarget); binding it disturbs no VAO state.
|
||||||
|
Bool EnsureScratchName(ScratchBuffer& buffer) {
|
||||||
|
if (buffer.id != 0) return true;
|
||||||
|
GLuint id = 0;
|
||||||
|
g_GLESFuncs.glGenBuffers(1, &id);
|
||||||
|
if (id == 0) return false;
|
||||||
|
buffer.id = id;
|
||||||
|
buffer.capacity = 0;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whole-buffer upload, for the two buffers that are read from offset 0 because they
|
||||||
|
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||||
|
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||||
|
// the old contents out of the same name.
|
||||||
|
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||||
|
if (bytes == 0) return true;
|
||||||
|
if (!EnsureScratchName(buffer)) return false;
|
||||||
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||||
|
// Grow in powers of two so a batch that creeps up in size stops respecifying.
|
||||||
|
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
|
||||||
|
while (capacity < bytes) capacity *= 2;
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||||
|
GL_STREAM_DRAW);
|
||||||
|
buffer.capacity = capacity;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
if (data) {
|
||||||
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring upload, for the buffers whose consumers can address a byte offset (indirect
|
||||||
|
// commands and rewritten index streams). Respecifying per batch is what an
|
||||||
|
// orphan-every-time scheme costs, and on a desktop-class driver that allocation
|
||||||
|
// dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages
|
||||||
|
// 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means
|
||||||
|
// one respecify per wrap; every byte between two wraps is written exactly once, so
|
||||||
|
// nothing in flight is overwritten, and the wrap itself orphans.
|
||||||
|
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||||
|
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||||
|
|
||||||
|
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||||
|
outOffset = 0;
|
||||||
|
if (bytes == 0) return true;
|
||||||
|
if (!EnsureScratchName(buffer)) return false;
|
||||||
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||||
|
|
||||||
|
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
|
||||||
|
if (buffer.capacity < aligned) {
|
||||||
|
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
|
||||||
|
while (capacity < aligned) capacity *= 2;
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||||
|
GL_STREAM_DRAW);
|
||||||
|
buffer.capacity = capacity;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
} else if (buffer.cursor + aligned > buffer.capacity) {
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(buffer.capacity),
|
||||||
|
nullptr, GL_STREAM_DRAW);
|
||||||
|
buffer.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
outOffset = buffer.cursor;
|
||||||
|
if (data) {
|
||||||
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||||
|
static_cast<GLsizeiptr>(bytes), data);
|
||||||
|
}
|
||||||
|
buffer.cursor += aligned;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier resolution
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Best-first, and measured rather than assumed. MobileGlues orders its own Auto
|
||||||
|
// multiindirect -> indirect -> basevertex; on both ES drivers available here that
|
||||||
|
// is backwards, because staging a command buffer per batch costs more than the
|
||||||
|
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
|
||||||
|
// ns/op, median of three:
|
||||||
|
//
|
||||||
|
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
|
||||||
|
// ext n/a 19300
|
||||||
|
// basevertex 2500 25200
|
||||||
|
// multiindirect 5700 27600
|
||||||
|
// drawelements 5600 28700
|
||||||
|
// indirect 5800 31000
|
||||||
|
//
|
||||||
|
// Ring-allocating the command staging (instead of respecifying per batch) was
|
||||||
|
// tried first and moved the indirect tiers by less than noise, so the cost is the
|
||||||
|
// indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry
|
||||||
|
// point rather than an indirect one - actually beats replaying the sub-draws.
|
||||||
|
//
|
||||||
|
// The compute tier is deliberately absent from the ladder: it rewrites the
|
||||||
|
// primitive stream rather than replaying it, and it measured slowest of all here,
|
||||||
|
// so it stays opt-in behind the env knob (the same call MobileGlues makes - its
|
||||||
|
// Auto never selects Compute either).
|
||||||
|
constexpr GLESMultiDrawMode kAutoLadder[] = {
|
||||||
|
GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect,
|
||||||
|
GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements,
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool SupportsTier(GLESMultiDrawMode tier) {
|
||||||
|
return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto;
|
||||||
|
Bool g_tierResolved = false;
|
||||||
|
String g_tierResolution;
|
||||||
|
|
||||||
|
void ResolveTierOnce() {
|
||||||
|
if (g_tierResolved) return;
|
||||||
|
g_tierResolved = true;
|
||||||
|
g_resolvedTier =
|
||||||
|
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||||
|
&g_tierResolution);
|
||||||
|
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||||
|
// The resolution line above says which tier was CHOSEN; this says which one a
|
||||||
|
// batch actually went through, and the two differ whenever a batch's shape
|
||||||
|
// demotes it. Worth a line each: a multi-draw path that resolves to a tier and
|
||||||
|
// then quietly runs a different one is exactly how "the batch drew nothing"
|
||||||
|
// hides.
|
||||||
|
Uint32 g_announcedTiers = 0;
|
||||||
|
|
||||||
|
void NoteTierExecuted(GLESMultiDrawMode tier) {
|
||||||
|
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||||
|
if (g_announcedTiers & bit) return;
|
||||||
|
g_announcedTiers |= bit;
|
||||||
|
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tier this particular batch can actually take. A tier is demoted here when
|
||||||
|
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||||
|
// its remaining feasibility checks inside its implementation, where the data it
|
||||||
|
// has to walk is already in hand.
|
||||||
|
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||||
|
Bool hasIndexBuffer) {
|
||||||
|
ResolveTierOnce();
|
||||||
|
GLESMultiDrawMode tier = g_resolvedTier;
|
||||||
|
|
||||||
|
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||||
|
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||||
|
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||||
|
// own index (the spec's value); nothing else observes the difference. The
|
||||||
|
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
|
||||||
|
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
|
||||||
|
// which hands the driver the whole basevertex array, can only leave ONE value
|
||||||
|
// in the uniform the shader reads.
|
||||||
|
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||||
|
tier == GLESMultiDrawMode::Compute;
|
||||||
|
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
|
||||||
|
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||||
|
: GLESMultiDrawMode::DrawElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The indirect tiers describe each sub-draw as an element offset into the
|
||||||
|
// bound element array buffer. A client-memory index array has no such buffer,
|
||||||
|
// and indirect draws are not defined without one.
|
||||||
|
if (!hasIndexBuffer &&
|
||||||
|
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
|
||||||
|
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||||
|
: GLESMultiDrawMode::DrawElements;
|
||||||
|
}
|
||||||
|
return tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Index rewriting, shared by the two tiers that fold base vertices into indices
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping
|
||||||
|
// the source width would be wrong, not merely tight: GL adds baseVertex to the
|
||||||
|
// index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past
|
||||||
|
// 65535 addresses a vertex the source type cannot spell. Widening also gives the
|
||||||
|
// rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase.
|
||||||
|
void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex,
|
||||||
|
Bool restartActive, Uint32 restartSentinel, Uint32* out) {
|
||||||
|
const Uint32 baseVertexBits = static_cast<Uint32>(baseVertex);
|
||||||
|
for (SizeT i = 0; i < sourceIndexCount; ++i) {
|
||||||
|
Uint32 value = 0;
|
||||||
|
switch (indexSize) {
|
||||||
|
case 1: value = source[i]; break;
|
||||||
|
case 2: {
|
||||||
|
Uint16 narrow = 0;
|
||||||
|
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
||||||
|
value = narrow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
|
||||||
|
}
|
||||||
|
// Unsigned wraparound is the defined behaviour for a negative base vertex.
|
||||||
|
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
|
||||||
|
// bound index buffer or straight from the client array. Null when the sub-draw
|
||||||
|
// would read outside the buffer.
|
||||||
|
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
|
||||||
|
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
|
||||||
|
SizeT indexCount, SizeT indexSize) {
|
||||||
|
if (!indexBuffer) {
|
||||||
|
return static_cast<const Uint8*>(indices);
|
||||||
|
}
|
||||||
|
if (!indexBufferBytes) return nullptr;
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices);
|
||||||
|
const SizeT byteEnd = byteOffset + indexCount * indexSize;
|
||||||
|
if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr;
|
||||||
|
return indexBufferBytes + byteOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: Ext - one glMultiDrawElementsBaseVertexEXT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount,
|
||||||
|
const GLint* basevertex) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::Ext)) return false;
|
||||||
|
const GLint* baseVertices = basevertex;
|
||||||
|
if (!baseVertices) {
|
||||||
|
// glMultiDrawElements: every base vertex is 0, but the entry point still
|
||||||
|
// wants an array. One permanently-zero vector serves every such batch.
|
||||||
|
if (g_zeroBaseVertices.size() < static_cast<SizeT>(drawcount)) {
|
||||||
|
g_zeroBaseVertices.resize(static_cast<SizeT>(drawcount), 0);
|
||||||
|
}
|
||||||
|
baseVertices = g_zeroBaseVertices.data();
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::Ext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tiers: MultiIndirect / Indirect - synthesized indirect commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
|
||||||
|
Bool feedBaseVertex) {
|
||||||
|
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return false;
|
||||||
|
// Indirect commands address indices as an element offset into the bound element
|
||||||
|
// array buffer, and an indirect draw is not defined without one.
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
if (!indexBuffer) return false;
|
||||||
|
|
||||||
|
g_commandStaging.resize(static_cast<SizeT>(drawcount));
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||||
|
// firstIndex counts elements, so an offset that is not a whole number of
|
||||||
|
// them cannot be expressed as a command at all.
|
||||||
|
if (byteOffset % indexSize != 0) return false;
|
||||||
|
auto& command = g_commandStaging[static_cast<SizeT>(i)];
|
||||||
|
command.count = count[i] > 0 ? static_cast<Uint32>(count[i]) : 0u;
|
||||||
|
command.instanceCount = 1;
|
||||||
|
command.firstIndex = static_cast<Uint32>(byteOffset / indexSize);
|
||||||
|
command.baseVertex = basevertex ? basevertex[i] : 0;
|
||||||
|
command.baseInstance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||||
|
SizeT commandBase = 0;
|
||||||
|
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every synthesized command carries baseInstance 0. Say so through the direct
|
||||||
|
// path, which also clears the indirect-params word index a preceding real
|
||||||
|
// indirect draw may have left pointing into its own command buffer.
|
||||||
|
SetCurrentBaseInstance(0);
|
||||||
|
|
||||||
|
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||||
|
if (batched) {
|
||||||
|
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||||
|
drawcount, 0);
|
||||||
|
} else {
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
|
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||||
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
|
}
|
||||||
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||||
|
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: BaseVertex - the per-sub-draw replay
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
|
basevertex ? basevertex[i] : 0);
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: DrawElements - base vertices folded into a scratch index stream
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
|
||||||
|
Bool feedBaseVertex) {
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return false;
|
||||||
|
|
||||||
|
SizeT total = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] > 0) total += static_cast<SizeT>(count[i]);
|
||||||
|
}
|
||||||
|
if (total == 0) return true;
|
||||||
|
if (total > kMaxFlattenedIndices) return false;
|
||||||
|
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
const Uint8* indexBufferBytes = nullptr;
|
||||||
|
SizeT indexBufferSize = 0;
|
||||||
|
if (indexBuffer) {
|
||||||
|
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||||
|
// a shader write may have moved past it since the last sync.
|
||||||
|
indexBuffer->SyncPersistentMappedRange();
|
||||||
|
indexBuffer->SyncGpuWrites();
|
||||||
|
indexBufferBytes = indexBuffer->MappedData();
|
||||||
|
indexBufferSize = indexBuffer->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bool restartActive = RestartActive();
|
||||||
|
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||||
|
g_indexStaging.resize(total);
|
||||||
|
SizeT cursor = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
|
||||||
|
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||||
|
subDrawCount, indexSize);
|
||||||
|
if (!source) {
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||||
|
"buffer; skipping the batch",
|
||||||
|
i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
|
||||||
|
restartSentinel, g_indexStaging.data() + cursor);
|
||||||
|
cursor += subDrawCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT indexBase = 0;
|
||||||
|
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id);
|
||||||
|
cursor = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
// The base vertex is folded into the rewritten index stream here, so the
|
||||||
|
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||||
|
// application passed for this sub-draw.
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
|
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||||
|
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||||
|
cursor += static_cast<SizeT>(count[i]);
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: Compute - the whole batch flattened into one rebased index stream
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// One index per invocation. The sub-draw an output slot belongs to is found by
|
||||||
|
// binary search over the inclusive prefix sums of the sub-draw counts, which is
|
||||||
|
// why the descriptors are sorted by construction. Sub-draws with a zero count
|
||||||
|
// repeat the previous prefix sum and are therefore skipped by the search.
|
||||||
|
//
|
||||||
|
// Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees
|
||||||
|
// four per compute stage, so the per-sub-draw descriptors share one buffer.
|
||||||
|
constexpr const char* kFlattenComputeSource = R"(#version 310 es
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
|
||||||
|
uniform uint uElementSize;
|
||||||
|
uniform uint uDrawCount;
|
||||||
|
uniform uint uTotalIndices;
|
||||||
|
|
||||||
|
layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; };
|
||||||
|
layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; };
|
||||||
|
layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; };
|
||||||
|
|
||||||
|
uint ReadSourceIndex(uint element) {
|
||||||
|
if (uElementSize == 4u) {
|
||||||
|
return sourceWords[element];
|
||||||
|
}
|
||||||
|
if (uElementSize == 2u) {
|
||||||
|
uint word = sourceWords[element >> 1u];
|
||||||
|
return (word >> ((element & 1u) * 16u)) & 0xFFFFu;
|
||||||
|
}
|
||||||
|
uint word = sourceWords[element >> 2u];
|
||||||
|
return (word >> ((element & 3u) * 8u)) & 0xFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint outIndex = gl_GlobalInvocationID.x;
|
||||||
|
if (outIndex >= uTotalIndices) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint low = 0u;
|
||||||
|
uint high = uDrawCount - 1u;
|
||||||
|
while (low < high) {
|
||||||
|
uint mid = low + (high - low) / 2u;
|
||||||
|
if (drawInfo[mid * 3u + 2u] > outIndex) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]);
|
||||||
|
// Unsigned wraparound is the defined behaviour for a negative base vertex. No
|
||||||
|
// restart sentinel handling: the tier declines outright while restart is enabled.
|
||||||
|
flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct FlattenedStream {
|
||||||
|
Uint bufferId = 0;
|
||||||
|
SizeT indexCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool EnsureComputeProgram() {
|
||||||
|
if (g_computeProgram != 0) return true;
|
||||||
|
if (g_computeProgramFailed) return false;
|
||||||
|
g_computeProgramFailed = true; // cleared again only on a complete success
|
||||||
|
|
||||||
|
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
if (shader == 0) {
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char* source = kFlattenComputeSource;
|
||||||
|
g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
g_GLESFuncs.glCompileShader(shader);
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
if (status != GL_TRUE) {
|
||||||
|
char log[1024] = {};
|
||||||
|
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||||
|
if (program == 0) {
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glAttachShader(program, shader);
|
||||||
|
g_GLESFuncs.glLinkProgram(program);
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||||
|
if (status != GL_TRUE) {
|
||||||
|
char log[1024] = {};
|
||||||
|
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||||
|
g_GLESFuncs.glDeleteProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_computeProgram = program;
|
||||||
|
g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize");
|
||||||
|
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||||
|
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||||
|
g_computeProgramFailed = false;
|
||||||
|
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the flattened stream, or leaves `out` empty when this batch's shape rules
|
||||||
|
// the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave
|
||||||
|
// the compute program current and the first storage points unbound; the
|
||||||
|
// preparation that follows re-establishes both.
|
||||||
|
void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::Compute)) return;
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return;
|
||||||
|
|
||||||
|
// Merging sub-draws into a single draw only reproduces the original primitive
|
||||||
|
// stream for list-shaped modes: a strip, loop or fan would gain primitives
|
||||||
|
// spanning the seam between two sub-draws.
|
||||||
|
const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode);
|
||||||
|
if (primitiveSize == 0) return;
|
||||||
|
|
||||||
|
// Primitive restart defeats the whole-multiple-of-a-primitive argument below,
|
||||||
|
// even for a list mode. A restart ends the current primitive, so a sub-draw of
|
||||||
|
// six GL_TRIANGLES indices with a restart after the third emits ONE triangle
|
||||||
|
// and drops the two leftover vertices - and once concatenated those leftovers
|
||||||
|
// find a third vertex in the next sub-draw and become a triangle that GL never
|
||||||
|
// draws. Splicing separator sentinels into the flattened stream could fix it,
|
||||||
|
// at the cost of a per-sub-draw offset the prefix-sum layout does not carry;
|
||||||
|
// declining is the honest trade for a tier that is already opt-in.
|
||||||
|
if (RestartActive()) return;
|
||||||
|
|
||||||
|
// The shader reads the source indices as a storage buffer, so there has to be
|
||||||
|
// a real buffer to read - a client-memory index array has none.
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
if (!indexBuffer) return;
|
||||||
|
|
||||||
|
// A dispatch inside an open capture span is not legal, and the span would also
|
||||||
|
// observe one merged draw rather than the batch it asked for.
|
||||||
|
if (XfbImpl::IsCaptureSpanOpen()) return;
|
||||||
|
|
||||||
|
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
|
||||||
|
if (!sourceResource || sourceResource->id == 0) return;
|
||||||
|
const SizeT sourceSize = indexBuffer->GetSize();
|
||||||
|
// std430 addresses the source as uint[]; a tail shorter than a word is not
|
||||||
|
// reachable, so a narrow index type needs a word-multiple buffer.
|
||||||
|
if (indexSize < 4 && (sourceSize % 4) != 0) return;
|
||||||
|
|
||||||
|
g_drawInfoStaging.resize(3 * static_cast<SizeT>(drawcount));
|
||||||
|
SizeT total = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const SizeT subDrawCount = count[i] > 0 ? static_cast<SizeT>(count[i]) : 0;
|
||||||
|
// GL drops a trailing partial primitive per sub-draw; concatenation would
|
||||||
|
// instead splice it onto the next sub-draw's first vertices.
|
||||||
|
if (subDrawCount % primitiveSize != 0) return;
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||||
|
if (byteOffset % indexSize != 0) return;
|
||||||
|
if (subDrawCount != 0) {
|
||||||
|
const SizeT byteEnd = byteOffset + subDrawCount * indexSize;
|
||||||
|
if (byteEnd > sourceSize || byteEnd < byteOffset) return;
|
||||||
|
}
|
||||||
|
total += subDrawCount;
|
||||||
|
if (total > kMaxComputeFlattenedIndices) return;
|
||||||
|
const SizeT slot = 3 * static_cast<SizeT>(i);
|
||||||
|
g_drawInfoStaging[slot] = static_cast<Uint32>(byteOffset / indexSize);
|
||||||
|
g_drawInfoStaging[slot + 1] = static_cast<Uint32>(basevertex ? basevertex[i] : 0);
|
||||||
|
g_drawInfoStaging[slot + 2] = static_cast<Uint32>(total);
|
||||||
|
}
|
||||||
|
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||||
|
|
||||||
|
if (!EnsureComputeProgram()) return;
|
||||||
|
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||||
|
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
|
||||||
|
|
||||||
|
g_GLESFuncs.glUseProgram(g_computeProgram);
|
||||||
|
PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram;
|
||||||
|
if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast<GLuint>(indexSize));
|
||||||
|
if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast<GLuint>(drawcount));
|
||||||
|
if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast<GLuint>(total));
|
||||||
|
|
||||||
|
g_GLESFuncs.glDispatchCompute(
|
||||||
|
static_cast<GLuint>((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1);
|
||||||
|
g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT);
|
||||||
|
|
||||||
|
// Hand the storage points back to their GL default. PrepareForDraw re-syncs
|
||||||
|
// only the points the app has actually touched, so leaving a scratch buffer on
|
||||||
|
// an untouched point would keep it visible to the next shader that declares one.
|
||||||
|
for (Uint point = 0; point < 3; ++point) {
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::Compute);
|
||||||
|
out.bufferId = g_flattenedIndices.id;
|
||||||
|
out.indexCount = total;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// Public surface
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
GLESMultiDrawMode tier) {
|
||||||
|
const Bool esAtLeast31 =
|
||||||
|
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Ext:
|
||||||
|
return caps.SupportsMultiDrawElementsBaseVertex;
|
||||||
|
case GLESMultiDrawMode::MultiIndirect:
|
||||||
|
return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||||
|
case GLESMultiDrawMode::Indirect:
|
||||||
|
return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||||
|
case GLESMultiDrawMode::BaseVertex:
|
||||||
|
return caps.SupportsDrawElementsBaseVertex;
|
||||||
|
case GLESMultiDrawMode::DrawElements:
|
||||||
|
// Plain glDrawElements over a rewritten index stream: ES 2 core, so this is
|
||||||
|
// the floor every other tier can fall back to.
|
||||||
|
return true;
|
||||||
|
case GLESMultiDrawMode::Compute:
|
||||||
|
// Three storage blocks, which is inside the four ES 3.1 guarantees per stage.
|
||||||
|
return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 &&
|
||||||
|
funcs.glBindBufferBase != nullptr;
|
||||||
|
case GLESMultiDrawMode::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||||
|
const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested,
|
||||||
|
String* explanation) {
|
||||||
|
const auto bestAuto = [&]() {
|
||||||
|
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||||
|
if (IsTierSupported(caps, funcs, tier)) return tier;
|
||||||
|
}
|
||||||
|
return GLESMultiDrawMode::DrawElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements;
|
||||||
|
String line;
|
||||||
|
if (requested == GLESMultiDrawMode::Auto) {
|
||||||
|
resolved = bestAuto();
|
||||||
|
line = String("auto -> ") + TierName(resolved);
|
||||||
|
} else if (IsTierSupported(caps, funcs, requested)) {
|
||||||
|
resolved = requested;
|
||||||
|
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved);
|
||||||
|
} else {
|
||||||
|
resolved = bestAuto();
|
||||||
|
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) +
|
||||||
|
" requested but unsupported by this driver -> " + TierName(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explanation) {
|
||||||
|
String supported;
|
||||||
|
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||||
|
if (!IsTierSupported(caps, funcs, tier)) continue;
|
||||||
|
if (!supported.empty()) supported += ", ";
|
||||||
|
supported += TierName(tier);
|
||||||
|
}
|
||||||
|
if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) {
|
||||||
|
supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)";
|
||||||
|
}
|
||||||
|
*explanation = line + " (driver supports: " + supported + ")";
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* TierName(GLESMultiDrawMode tier) {
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Auto: return "auto";
|
||||||
|
case GLESMultiDrawMode::Ext: return "ext";
|
||||||
|
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
|
||||||
|
case GLESMultiDrawMode::Indirect: return "indirect";
|
||||||
|
case GLESMultiDrawMode::BaseVertex: return "basevertex";
|
||||||
|
case GLESMultiDrawMode::DrawElements: return "drawelements";
|
||||||
|
case GLESMultiDrawMode::Compute: return "compute";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode ResolvedTier() {
|
||||||
|
ResolveTierOnce();
|
||||||
|
return g_resolvedTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
String DescribeTierResolution() {
|
||||||
|
ResolveTierOnce();
|
||||||
|
return g_tierResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnBackendContextDestroyed() {
|
||||||
|
g_indirectCommands = {};
|
||||||
|
g_rebasedIndices = {};
|
||||||
|
g_drawInfo = {};
|
||||||
|
g_flattenedIndices = {};
|
||||||
|
g_computeProgram = 0;
|
||||||
|
g_computeProgramFailed = false;
|
||||||
|
g_uElementSize = -1;
|
||||||
|
g_uDrawCount = -1;
|
||||||
|
g_uTotalIndices = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
|
if (drawcount <= 0 || !count || !indices) return;
|
||||||
|
// State-independent and possibly throwing, so it runs before any GL work.
|
||||||
|
CheckPrimitiveRestartSupported(type);
|
||||||
|
|
||||||
|
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||||
|
|
||||||
|
// The compute tier dispatches BEFORE the draw state is established: doing it
|
||||||
|
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||||
|
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||||
|
// span is not legal at all. On success it hands back a flattened index stream.
|
||||||
|
// A batch whose sub-draws carry their own base vertices cannot be flattened either
|
||||||
|
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
|
||||||
|
// Asked conservatively because this decision precedes PrepareForDraw - see
|
||||||
|
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
|
||||||
|
// once the batch is one draw the values are gone, whereas declining to flatten only
|
||||||
|
// costs the unrolled tier.
|
||||||
|
FlattenedStream flattened;
|
||||||
|
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
|
||||||
|
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
|
||||||
|
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareForDraw(DrawSyncBit::IndexBuffer);
|
||||||
|
|
||||||
|
if (flattened.indexCount != 0) {
|
||||||
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||||
|
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
|
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 feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||||
|
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||||
|
|
||||||
|
Bool drawn = false;
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Ext:
|
||||||
|
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::MultiIndirect:
|
||||||
|
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
|
||||||
|
feedBaseVertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Indirect:
|
||||||
|
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
|
||||||
|
feedBaseVertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::BaseVertex:
|
||||||
|
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::DrawElements:
|
||||||
|
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||||
|
feedBaseVertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Compute:
|
||||||
|
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Auto:
|
||||||
|
break; // resolution never yields Auto
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||||
|
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||||
|
// rewritten index stream where it does not. Both are safe for any batch these
|
||||||
|
// entry points can receive.
|
||||||
|
if (!drawn) {
|
||||||
|
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||||
|
}
|
||||||
|
if (!drawn) {
|
||||||
|
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||||
|
feedBaseVertex);
|
||||||
|
}
|
||||||
|
if (!drawn) {
|
||||||
|
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||||
|
"the batch was dropped",
|
||||||
|
drawcount, mode, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
#include <Config.h>
|
||||||
|
#include "DirectGLES.h"
|
||||||
|
|
||||||
|
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
|
||||||
|
// points on OpenGL ES, which has neither in core.
|
||||||
|
//
|
||||||
|
// Every strategy below is an emulation; they differ only in which driver capability
|
||||||
|
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
|
||||||
|
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
|
||||||
|
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
|
||||||
|
//
|
||||||
|
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
|
||||||
|
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
|
||||||
|
// Indirect N x glDrawElementsIndirect N + 1 upload
|
||||||
|
// BaseVertex N x glDrawElementsBaseVertex N
|
||||||
|
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
|
||||||
|
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
|
||||||
|
// rebased index stream
|
||||||
|
//
|
||||||
|
// Which one runs is resolved once per ES context from the driver's capabilities,
|
||||||
|
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
|
||||||
|
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
|
||||||
|
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||||
|
// The tier this ES context resolved to, computed on first use and stable after.
|
||||||
|
MG_Config::GLESMultiDrawMode ResolvedTier();
|
||||||
|
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
|
||||||
|
const char* TierName(MG_Config::GLESMultiDrawMode tier);
|
||||||
|
// One line naming the resolved tier, the tiers the driver can support, and the env
|
||||||
|
// clamp if one applied. For DriverPost and the startup log.
|
||||||
|
String DescribeTierResolution();
|
||||||
|
|
||||||
|
// The resolution itself, as a pure function of a capability set: the backend feeds
|
||||||
|
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
|
||||||
|
// standalone, and both therefore report the same tier. `explanation`, when non-null,
|
||||||
|
// receives the "requested -> resolved (driver supports: ...)" line.
|
||||||
|
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||||
|
const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
MG_Config::GLESMultiDrawMode requested, String* explanation);
|
||||||
|
// Whether one tier is runnable on the given capability set, for per-row POST output.
|
||||||
|
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
MG_Config::GLESMultiDrawMode tier);
|
||||||
|
|
||||||
|
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
|
||||||
|
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
|
||||||
|
// base vertex is 0). Owns the whole draw, preparation included: callers must not
|
||||||
|
// have run PrepareForDraw, because the compute tier has to dispatch before the
|
||||||
|
// draw state is established.
|
||||||
|
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex);
|
||||||
|
|
||||||
|
// The ES context is gone: every scratch buffer and the compute program belonged to
|
||||||
|
// it, so drop the names without deleting them (the dead context reclaims them).
|
||||||
|
void OnBackendContextDestroyed();
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||||
@@ -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>
|
||||||
@@ -61,29 +62,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
|
|
||||||
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
|
|
||||||
// ES texture format but not a legal multisample storage format. Widening to four channels
|
|
||||||
// is safe here precisely because there is no transfer path that would have to expand
|
|
||||||
// three-channel client data, and the alpha the draw writes for a three-channel source is
|
|
||||||
// already the 1.0 the frontend format implies.
|
|
||||||
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
|
||||||
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
|
|
||||||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
|
||||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
|
||||||
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
|
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||||
SizeT targetIndex,
|
SizeT targetIndex,
|
||||||
Bool caveat,
|
Bool caveat,
|
||||||
@@ -141,14 +119,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
options = GetRuntimeFallbackNormalizeOptions(
|
||||||
GetRenderTargetNormalizeOptions(targetIndex));
|
requestedInternalFormat,
|
||||||
|
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||||
}
|
}
|
||||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
|
// Every image that can back a colour attachment needs a colour-renderable storage format,
|
||||||
|
// and ES has no renderable three-channel format at all: a three-channel float fallback is
|
||||||
|
// a legal ES texture but neither legal multisample storage nor a legal attachment, so
|
||||||
|
// GL_RGB8_SNORM / GL_RGB16F / ... have to be widened to four channels for any of them.
|
||||||
|
// This used to cover the multisample pair alone, on the grounds that only those can never
|
||||||
|
// be uploaded to; the transfer paths now expand three-channel client data themselves
|
||||||
|
// (Managers.cpp PrepareFallbackUpload) and hide the added alpha again on sample and
|
||||||
|
// readback, so the same substitution is available everywhere.
|
||||||
|
//
|
||||||
|
// The widening only ever *happens* where the driver refuses the native form (see
|
||||||
|
// PopulateFormatCapabilitiesImpl: outside multisample storage it rides the driver branch,
|
||||||
|
// behind the native probe), so a driver that does render to a three-channel image keeps
|
||||||
|
// allocating it byte for byte.
|
||||||
|
//
|
||||||
|
// Do NOT read that as "nothing changes off-device". Measured on Mesa 26.1.6 llvmpipe
|
||||||
|
// (the headless CI driver), an ES 3.2 GL_TEXTURE_2D colour attachment is COMPLETE for
|
||||||
|
// GL_RGB8 and GL_RGB16F but INCOMPLETE_ATTACHMENT for GL_RGB8_SNORM, GL_SRGB8 and every
|
||||||
|
// RGB integer format, and UNSUPPORTED for GL_RGB32F. Those eight formats therefore DO
|
||||||
|
// take the widened path on llvmpipe, which is where the retrace fixtures and the glcts
|
||||||
|
// green suites run - the substitution is driver-conditional, not desktop-exempt.
|
||||||
|
//
|
||||||
|
// A buffer texture is the one image that can never be an attachment; its storage is the
|
||||||
|
// buffer object's, and widening it would misdescribe the application's data.
|
||||||
|
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
||||||
|
if (targetIndex >= kFormatCapabilityTargetCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return static_cast<TextureTarget>(targetIndex) != TextureTarget::TextureBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||||
|
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex) {
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
|
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
|
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||||
|
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -178,20 +203,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Bool BackendFormatAddsAlpha(TextureInternalFormat internalFormat, SizeT targetIndex) {
|
||||||
|
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
|
||||||
|
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||||
|
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||||
const SizeT targetIndex =
|
const SizeT targetIndex =
|
||||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
return BackendFormatAddsAlpha(internalFormat, targetIndex);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||||
return false;
|
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
|
||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
|
||||||
const Flags<PixelFormatNormalizeOptionBit> options =
|
|
||||||
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
|
||||||
GetRenderTargetNormalizeOptions(targetIndex));
|
|
||||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
|
||||||
}
|
}
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
@@ -383,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");
|
||||||
@@ -401,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);
|
||||||
@@ -437,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.
|
||||||
@@ -494,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
|
||||||
@@ -555,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.
|
||||||
@@ -605,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,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).
|
||||||
@@ -1114,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
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
namespace DebugImpl {
|
namespace DebugImpl {
|
||||||
@@ -34,6 +36,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace VertexArrayImpl
|
} // namespace VertexArrayImpl
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
|
// Whether images on this format-capability target can back a colour attachment, and so
|
||||||
|
// need a colour-renderable storage format even when the frontend asked for a
|
||||||
|
// three-channel one ES never renders to. Shared by the capability probe (which passes the
|
||||||
|
// capabilities it has just queried, before the globals are published) and by the
|
||||||
|
// allocation path (which reads the active backend's), so the format the cache was probed
|
||||||
|
// with is always the format the image is created with.
|
||||||
|
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||||
|
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||||
|
|
||||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType,
|
GLenum* outFormat, GLenum* outType,
|
||||||
TextureTarget target = TextureTarget::Unknown);
|
TextureTarget target = TextureTarget::Unknown);
|
||||||
@@ -41,10 +53,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLenum* outFormat, GLenum* outType);
|
GLenum* outFormat, GLenum* outType);
|
||||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
|
|
||||||
// True when the format the texture is actually created with has an alpha channel the
|
// True when the format the image is actually created with has an alpha channel the
|
||||||
// frontend format does not (the three-channel multisample widening). GL reads such a
|
// frontend format does not (the three-channel colour-renderable widening). GL reads such
|
||||||
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
|
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
|
||||||
|
// any readback of the image has to overwrite the alpha the draw happened to leave there.
|
||||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
@@ -101,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 {
|
||||||
@@ -116,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_";
|
||||||
@@ -128,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 {
|
||||||
|
|||||||
@@ -9,13 +9,16 @@
|
|||||||
#include "BackendObject_DirectVulkan.h"
|
#include "BackendObject_DirectVulkan.h"
|
||||||
#include "MG_Backend/BackendObject.h"
|
#include "MG_Backend/BackendObject.h"
|
||||||
#include "DirectVulkan.h"
|
#include "DirectVulkan.h"
|
||||||
|
#include "SubgroupSupportPolicy.h"
|
||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
|
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -382,6 +385,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
UpdateAdvertisedExtensions();
|
UpdateAdvertisedExtensions();
|
||||||
|
if (MG_State::pGLContext) {
|
||||||
|
MG_State::pGLContext->InvalidateCompileEnv();
|
||||||
|
}
|
||||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||||
MutableFormatCapabilities());
|
MutableFormatCapabilities());
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -496,33 +502,75 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.ExtraVendor = Nullopt,
|
.ExtraVendor = Nullopt,
|
||||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||||
.TargetGLSLVersion = {4, 6, 0},
|
.TargetGLSLVersion = {4, 6, 0},
|
||||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||||
.IsCompatibilityProfile = false},
|
.IsCompatibilityProfile = false},
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||||
return rendererInfo;
|
return rendererInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported) {
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||||
Vector<GLExtension> extensions = {
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||||
|
E_GL_ARB_multi_draw_indirect,
|
||||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||||
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
E_GL_ARB_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.
|
||||||
E_GL_ARB_get_program_binary};
|
E_GL_ARB_get_program_binary};
|
||||||
|
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||||
|
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||||
|
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||||
|
// be rebased to OpenGL's zero-based semantics.
|
||||||
|
if (nonZeroIndirectBaseInstanceSupported) {
|
||||||
|
extensions.push_back(E_GL_ARB_base_instance);
|
||||||
|
}
|
||||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||||
|
// device's: the compiler threads belong to MobileGL's shader pool and
|
||||||
|
// glCompileShader/glLinkProgram are serviced entirely inside the frontend, so there
|
||||||
|
// is no device feature to condition this on.
|
||||||
|
//
|
||||||
|
// Gated on the async flag deliberately, and this is the whole reason the gate
|
||||||
|
// exists. Advertising the string is the one part of asynchronous compilation that a
|
||||||
|
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
|
||||||
|
// the moment they see it - they enqueue whole pipeline batches and poll
|
||||||
|
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
|
||||||
|
// change as well as the threading, or the kill switch would only be half a switch.
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
|
}
|
||||||
|
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). 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.
|
||||||
@@ -601,12 +649,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
|
||||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
|
||||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
|
||||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
|
||||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||||
funcsTable.GL.FenceSync = FenceSync;
|
funcsTable.GL.FenceSync = FenceSync;
|
||||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||||
@@ -650,6 +692,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_vulkanCaps = capabilities;
|
m_vulkanCaps = capabilities;
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
UpdateAdvertisedExtensions();
|
UpdateAdvertisedExtensions();
|
||||||
|
if (MG_State::pGLContext) {
|
||||||
|
MG_State::pGLContext->InvalidateCompileEnv();
|
||||||
|
}
|
||||||
MutableFormatCapabilities().Clear();
|
MutableFormatCapabilities().Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,9 +705,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||||
// the whole list keeps re-runs idempotent.
|
// the whole list keeps re-runs idempotent.
|
||||||
|
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||||
|
// extension by itself on devices with no native subgroup support at all; a
|
||||||
|
// device with native subgroups always advertises - and uses - those.
|
||||||
|
const Bool subgroupSupportAdvertised =
|
||||||
|
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||||
|
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||||
|
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||||
@@ -758,14 +810,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);
|
||||||
@@ -833,7 +919,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) {
|
||||||
@@ -842,6 +948,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||||
|
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||||
|
// advertised values describe the 32-lane virtual subgroup the compute
|
||||||
|
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||||
|
// GL requires the advertisement and the execution to agree, and on this
|
||||||
|
// path the emulation is what executes; only the compute stage is offered.
|
||||||
|
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
||||||
|
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||||
|
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||||
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||||
|
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||||
|
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||||
} else {
|
} else {
|
||||||
m_dynamicParameters.SubgroupSize = 0;
|
m_dynamicParameters.SubgroupSize = 0;
|
||||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// POST screen shows.
|
// POST screen shows.
|
||||||
|
|
||||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
// capabilities). A live backend copies this in its constructor and
|
||||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||||
// exist; callers that need the advertised list for a known capability set must
|
// exist; callers that need the advertised list for a known capability set must
|
||||||
// use BuildAdvertisedExtensions instead.
|
// use BuildAdvertisedExtensions instead.
|
||||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||||
// the detected device support (passing an already-gated value is harmless).
|
// the detected device support (passing an already-gated value is harmless).
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported);
|
Bool anisotropicFilteringSupported,
|
||||||
|
Bool nonZeroIndirectBaseInstanceSupported);
|
||||||
|
|
||||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// slot's ownership unambiguous.
|
// slot's ownership unambiguous.
|
||||||
Uint64 programLifetimeId = 0;
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 backendStateVersion = 0;
|
Uint32 backendStateVersion = 0;
|
||||||
|
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
|
||||||
|
// version, and the pipeline composite is unnamed so the in-place patch in
|
||||||
|
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
|
||||||
|
// mirror replay bumps only the program's block-binding version. Without this
|
||||||
|
// key the composite's slot kept serving the pre-rebind block.binding.
|
||||||
|
Uint32 blockBindingVersion = 0;
|
||||||
Vector<StorageBlockResource> storageBlocks;
|
Vector<StorageBlockResource> storageBlocks;
|
||||||
Vector<BufferVariableResource> bufferVariables;
|
Vector<BufferVariableResource> bufferVariables;
|
||||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||||
@@ -156,18 +162,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||||
|
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
|
||||||
// The lifetime id must match too: a new program that reuses a deleted
|
// The lifetime id must match too: a new program that reuses a deleted
|
||||||
// program's name and happens to land on the same backendStateVersion (both
|
// program's name and happens to land on the same backendStateVersion (both
|
||||||
// count from zero) would otherwise be served the dead program's reflection.
|
// count from zero) would otherwise be served the dead program's reflection.
|
||||||
if (cache.programLifetimeId == programLifetimeId &&
|
if (cache.programLifetimeId == programLifetimeId &&
|
||||||
cache.backendStateVersion == backendStateVersion &&
|
cache.backendStateVersion == backendStateVersion &&
|
||||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||||
|
if (cache.blockBindingVersion != blockBindingVersion) {
|
||||||
|
// Only the block bindings moved (glShaderStorageBlockBinding, or the
|
||||||
|
// pipeline composite's mirror replay - neither touches the backend
|
||||||
|
// state version): the reflection itself is unchanged, so re-apply the
|
||||||
|
// overrides by name instead of re-running spirv-reflect. Overrides
|
||||||
|
// only ever accumulate, so a block without one still holds its
|
||||||
|
// declared binding.
|
||||||
|
for (auto& block : cache.storageBlocks) {
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
|
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||||
|
}
|
||||||
|
cache.blockBindingVersion = blockBindingVersion;
|
||||||
|
}
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = {};
|
cache = {};
|
||||||
cache.programLifetimeId = programLifetimeId;
|
cache.programLifetimeId = programLifetimeId;
|
||||||
cache.backendStateVersion = backendStateVersion;
|
cache.backendStateVersion = backendStateVersion;
|
||||||
|
cache.blockBindingVersion = blockBindingVersion;
|
||||||
|
|
||||||
Vector<SpvReflectShaderModule> modules;
|
Vector<SpvReflectShaderModule> modules;
|
||||||
Vector<Bool> validModules;
|
Vector<Bool> validModules;
|
||||||
@@ -232,6 +253,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
StorageBlockResource block{};
|
StorageBlockResource block{};
|
||||||
block.name = blockName;
|
block.name = blockName;
|
||||||
block.binding = binding->binding;
|
block.binding = binding->binding;
|
||||||
|
// glShaderStorageBlockBinding survives every rebuild of this cache: the
|
||||||
|
// authoritative record of a rebound block lives on the program (it is what
|
||||||
|
// GL_BUFFER_BINDING reports), and only the shader's declared binding is
|
||||||
|
// recoverable from the SPIR-V. Without this, any unrelated state-version
|
||||||
|
// bump would silently revert the block to its declared binding.
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(blockName);
|
||||||
|
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||||
block.dataSize = static_cast<GLint>(binding->block.size);
|
block.dataSize = static_cast<GLint>(binding->block.size);
|
||||||
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
|
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
|
||||||
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
|
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
|
||||||
@@ -256,132 +284,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return programObject.get();
|
return programObject.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyResourceName(const String& source, GLsizei bufSize, GLsizei* length, GLchar* name) {
|
|
||||||
const GLsizei writtenLength = static_cast<GLsizei>(source.size());
|
|
||||||
if (length) {
|
|
||||||
*length = writtenLength;
|
|
||||||
}
|
|
||||||
if (name && bufSize > 0) {
|
|
||||||
const GLsizei copyLength = std::min<GLsizei>(bufSize - 1, writtenLength);
|
|
||||||
std::memcpy(name, source.data(), static_cast<SizeT>(copyLength));
|
|
||||||
name[copyLength] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
if (drawBuffer) {
|
if (drawBuffer) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reinterpret_cast<const Uint8*>(indirect);
|
return reinterpret_cast<const Uint8*>(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLuint> GetUniformBlockActiveVariables(const MG_State::GLState::ProgramObject& program,
|
|
||||||
GLuint blockIndex) {
|
|
||||||
Vector<GLuint> activeVariables;
|
|
||||||
const Uint uniformCount = program.GetUniformCount();
|
|
||||||
activeVariables.reserve(uniformCount);
|
|
||||||
for (Uint uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) {
|
|
||||||
if (program.GetActiveUniformBlockIndex(uniformIndex) == static_cast<Int>(blockIndex)) {
|
|
||||||
activeVariables.push_back(uniformIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return activeVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint FindProgramInputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveAttributesCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveAttribName(index) == name) {
|
|
||||||
return static_cast<GLuint>(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint FindProgramOutputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
|
||||||
return static_cast<GLuint>(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramOutputLocation(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
|
||||||
return program.GetFragmentOutputLocation(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceActiveCount(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
|
||||||
const ProgramResourceCache& cache) {
|
|
||||||
switch (programInterface) {
|
|
||||||
case GL_SHADER_STORAGE_BLOCK:
|
|
||||||
return static_cast<GLint>(cache.storageBlocks.size());
|
|
||||||
case GL_BUFFER_VARIABLE:
|
|
||||||
return static_cast<GLint>(cache.bufferVariables.size());
|
|
||||||
case GL_UNIFORM_BLOCK:
|
|
||||||
return program.GetActiveUniformBlocksCount();
|
|
||||||
case GL_UNIFORM:
|
|
||||||
return static_cast<GLint>(program.GetUniformCount());
|
|
||||||
case GL_PROGRAM_INPUT:
|
|
||||||
return program.GetActiveAttributesCount();
|
|
||||||
case GL_PROGRAM_OUTPUT:
|
|
||||||
return program.GetActiveFragmentOutputCount();
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceMaxNameLength(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
|
||||||
const ProgramResourceCache& cache) {
|
|
||||||
switch (programInterface) {
|
|
||||||
case GL_SHADER_STORAGE_BLOCK: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
for (const auto& block : cache.storageBlocks) maxLength = std::max(maxLength, block.name.size() + 1);
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
case GL_BUFFER_VARIABLE: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
for (const auto& var : cache.bufferVariables) maxLength = std::max(maxLength, var.name.size() + 1);
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
case GL_UNIFORM_BLOCK:
|
|
||||||
return program.GetActiveUniformBlocksMaxNameLength() + 1;
|
|
||||||
case GL_UNIFORM:
|
|
||||||
return program.GetUniformMaxLength() + 1;
|
|
||||||
case GL_PROGRAM_INPUT:
|
|
||||||
return program.GetActiveAttributesMaxLength() + 1;
|
|
||||||
case GL_PROGRAM_OUTPUT: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
maxLength = std::max(maxLength, program.GetActiveFragmentOutputName(index).size() + 1);
|
|
||||||
}
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ClearProgramResourceCaches() {
|
void ClearProgramResourceCaches() {
|
||||||
@@ -395,11 +317,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||||
auto& cache = GetProgramResourceCache(program);
|
auto& cache = GetProgramResourceCache(program);
|
||||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
auto find = [&cache](const String& key) {
|
||||||
[&](const StorageBlockResource& block) { return block.name == name; });
|
return std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||||
return it == cache.storageBlocks.end()
|
[&](const StorageBlockResource& block) { return block.name == key; });
|
||||||
? GL_INVALID_INDEX
|
};
|
||||||
: static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
auto it = find(name);
|
||||||
|
if (it == cache.storageBlocks.end()) {
|
||||||
|
// Cache names are normalized (NormalizeDescriptorName drops the array suffix), so
|
||||||
|
// an arrayed block that GL enumerates per element - "B[0]", "B[1]" - is one entry
|
||||||
|
// here, spelled "B". Retry against the bare name before giving up.
|
||||||
|
const auto bracket = name.rfind('[');
|
||||||
|
if (bracket == String::npos || name.empty() || name.back() != ']') return GL_INVALID_INDEX;
|
||||||
|
it = find(name.substr(0, bracket));
|
||||||
|
if (it == cache.storageBlocks.end()) return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
return static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
|
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
|
||||||
@@ -487,7 +419,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;
|
||||||
}
|
}
|
||||||
@@ -535,20 +467,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +534,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,357 +806,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||||
if (!params) return;
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||||
if (!programObject) return;
|
if (!programObject || storageBlockName == nullptr) return;
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
switch (pname) {
|
|
||||||
case GL_ACTIVE_RESOURCES:
|
|
||||||
*params = GetProgramResourceActiveCount(*programObject, programInterface, cache);
|
|
||||||
return;
|
|
||||||
case GL_MAX_NAME_LENGTH:
|
|
||||||
*params = GetProgramResourceMaxNameLength(*programObject, programInterface, cache);
|
|
||||||
return;
|
|
||||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
|
||||||
SizeT maxCount = 0;
|
|
||||||
for (const auto& block : cache.storageBlocks) {
|
|
||||||
maxCount = std::max(maxCount, block.activeVariables.size());
|
|
||||||
}
|
|
||||||
*params = static_cast<GLint>(maxCount);
|
|
||||||
} else if (programInterface == GL_UNIFORM_BLOCK) {
|
|
||||||
GLint maxCount = 0;
|
|
||||||
const Int activeBlocks = programObject->GetActiveUniformBlocksCount();
|
|
||||||
for (Int index = 0; index < activeBlocks; ++index) {
|
|
||||||
maxCount = std::max(maxCount, programObject->GetUniformBlockActiveUniformCount(index));
|
|
||||||
}
|
|
||||||
*params = maxCount;
|
|
||||||
} else {
|
|
||||||
*params = 0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
*params = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
if (!name) return GL_INVALID_INDEX;
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return GL_INVALID_INDEX;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
const String resourceName = name;
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
|
||||||
return GetShaderStorageBlockIndex(*programObject, name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_BUFFER_VARIABLE) {
|
|
||||||
const auto it = std::find_if(cache.bufferVariables.begin(), cache.bufferVariables.end(),
|
|
||||||
[&](const BufferVariableResource& var) { return var.name == resourceName; });
|
|
||||||
return it == cache.bufferVariables.end()
|
|
||||||
? GL_INVALID_INDEX
|
|
||||||
: static_cast<GLuint>(std::distance(cache.bufferVariables.begin(), it));
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM_BLOCK) {
|
|
||||||
return programObject->GetUniformBlockIndex(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM) {
|
|
||||||
const Int activeUniformIndex = programObject->GetActiveUniformIndex(resourceName);
|
|
||||||
return activeUniformIndex >= 0 ? static_cast<GLuint>(activeUniformIndex) : GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT) {
|
|
||||||
return FindProgramInputIndex(*programObject, resourceName);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return FindProgramOutputIndex(*programObject, resourceName);
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
|
||||||
GLsizei* length, GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
|
||||||
CopyResourceName(cache.storageBlocks[index].name, bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
|
||||||
CopyResourceName(cache.bufferVariables[index].name, bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM_BLOCK && programObject->IsActiveUniformBlock(index)) {
|
|
||||||
CopyResourceName(programObject->GetUniformBlockName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
|
||||||
CopyResourceName(programObject->GetActiveUniformName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT && index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
|
||||||
CopyResourceName(programObject->GetActiveAttribName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
|
||||||
CopyResourceName(programObject->GetActiveFragmentOutputName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (length) *length = 0;
|
|
||||||
if (name && bufSize > 0) name[0] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !props || !params || bufSize <= 0) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
GLsizei written = 0;
|
|
||||||
auto writeValue = [&](GLint value) {
|
|
||||||
if (written < bufSize) {
|
|
||||||
params[written++] = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (GLsizei propIndex = 0; propIndex < propCount; ++propIndex) {
|
|
||||||
const GLenum prop = props[propIndex];
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
|
||||||
const auto& block = cache.storageBlocks[index];
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(block.name.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_BINDING:
|
|
||||||
writeValue(static_cast<GLint>(block.binding));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_DATA_SIZE:
|
|
||||||
writeValue(block.dataSize);
|
|
||||||
break;
|
|
||||||
case GL_NUM_ACTIVE_VARIABLES:
|
|
||||||
writeValue(static_cast<GLint>(block.activeVariables.size()));
|
|
||||||
break;
|
|
||||||
case GL_ACTIVE_VARIABLES:
|
|
||||||
for (const auto variable : block.activeVariables) writeValue(static_cast<GLint>(variable));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
|
||||||
const auto& var = cache.bufferVariables[index];
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(var.name.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(GL_FLOAT);
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(1);
|
|
||||||
break;
|
|
||||||
case GL_OFFSET:
|
|
||||||
writeValue(var.offset);
|
|
||||||
break;
|
|
||||||
case GL_BLOCK_INDEX:
|
|
||||||
writeValue(static_cast<GLint>(var.blockIndex));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_STRIDE:
|
|
||||||
case GL_MATRIX_STRIDE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
|
||||||
case GL_IS_ROW_MAJOR:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_UNIFORM_BLOCK &&
|
|
||||||
programObject->IsActiveUniformBlock(index)) {
|
|
||||||
const auto activeVariables = GetUniformBlockActiveVariables(*programObject, index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockName(index).size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_BINDING:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockBinding(index)));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_DATA_SIZE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUBOSizeAt(index)));
|
|
||||||
break;
|
|
||||||
case GL_NUM_ACTIVE_VARIABLES:
|
|
||||||
writeValue(static_cast<GLint>(activeVariables.size()));
|
|
||||||
break;
|
|
||||||
case GL_ACTIVE_VARIABLES:
|
|
||||||
for (const GLuint variableIndex : activeVariables) {
|
|
||||||
writeValue(static_cast<GLint>(variableIndex));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangVertex) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangFragment) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangCompute) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
writeValue(GL_FALSE);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
|
||||||
const auto& uniformName = programObject->GetActiveUniformName(index);
|
|
||||||
const GLint location = programObject->GetUniformLocation(uniformName);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(uniformName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetActiveUniformType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveUniformArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_BLOCK_INDEX:
|
|
||||||
writeValue(programObject->GetActiveUniformBlockIndex(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(location);
|
|
||||||
break;
|
|
||||||
case GL_OFFSET:
|
|
||||||
writeValue(location >= 0 && programObject->IsValidUniformLocation(location)
|
|
||||||
? static_cast<GLint>(programObject->GetUniformOffset(location))
|
|
||||||
: 0);
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_STRIDE:
|
|
||||||
case GL_MATRIX_STRIDE:
|
|
||||||
case GL_IS_ROW_MAJOR:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_PROGRAM_INPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
|
||||||
const auto& resourceName = programObject->GetActiveAttribName(index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetActiveAttribType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveAttribArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(programObject->GetAttributeLocation(resourceName));
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
writeValue(GL_TRUE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
case GL_IS_PER_PATCH:
|
|
||||||
case GL_LOCATION_INDEX:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_PROGRAM_OUTPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
|
||||||
const auto& resourceName = programObject->GetActiveFragmentOutputName(index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetFragmentOutputType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveFragmentOutputArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(programObject->GetFragmentOutputLocation(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION_INDEX:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
writeValue(GL_TRUE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
case GL_IS_PER_PATCH:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeValue(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (length) *length = written;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !name) return -1;
|
|
||||||
if (programInterface == GL_UNIFORM) {
|
|
||||||
return programObject->GetUniformLocation(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT) {
|
|
||||||
return programObject->GetAttributeLocation(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return GetProgramOutputLocation(*programObject, name);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !name) return -1;
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return GetProgramOutputLocation(*programObject, name) >= 0 ? 0 : -1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
const Int maxBindings = pActiveBackendObject
|
const Int maxBindings = pActiveBackendObject
|
||||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||||
: 0;
|
: 0;
|
||||||
@@ -1234,13 +818,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (storageBlockIndex >= cache.storageBlocks.size()) {
|
// The frontend already validated that the name denotes an active block, and has
|
||||||
MG_State::pGLContext->RecordError(
|
// already recorded the new binding on the program - which is what reseeds this cache
|
||||||
ErrorCode::InvalidValue,
|
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
|
||||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage block index is not active."));
|
// (the common case: the very next draw reads it) from having to be thrown away.
|
||||||
return;
|
//
|
||||||
}
|
// Resolve the index BEFORE taking the reference, and bounds-check the way the
|
||||||
cache.storageBlocks[storageBlockIndex].binding = storageBlockBinding;
|
// sibling getter does. GetShaderStorageBlockIndex re-enters GetProgramResourceCache,
|
||||||
|
// which indexes g_programResourceCaches and can therefore insert - and that map is
|
||||||
|
// open-addressed, so a rehash MOVES its entries and a reference taken before the
|
||||||
|
// call is left dangling. Binding a program's storage block
|
||||||
|
// while another program's entry was still absent from the cache was a reproducible
|
||||||
|
// segfault (ProgramPipelineScenario's two storage-block cases, in one process).
|
||||||
|
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
|
||||||
|
if (blockIndex == GL_INVALID_INDEX) return;
|
||||||
|
auto& cache = GetProgramResourceCache(*programObject);
|
||||||
|
if (blockIndex >= cache.storageBlocks.size()) return;
|
||||||
|
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
||||||
}
|
}
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
||||||
@@ -1393,31 +987,89 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pVulkanRenderer->MultiDrawArrays(payload);
|
pVulkanRenderer->MultiDrawArrays(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared body of glMultiDrawElements (basevertex == nullptr) and
|
||||||
|
// glMultiDrawElementsBaseVertex: identical calls except for the per-draw
|
||||||
|
// vertex offset, which VkMultiDrawIndexedInfoEXT / VkDrawIndexedIndirectCommand /
|
||||||
|
// vkCmdDrawIndexed all carry natively.
|
||||||
|
static void MultiDrawElementsImpl(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
|
if (drawcount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no element-array buffer bound, every indices[i] is a client pointer into a
|
||||||
|
// separate CPU allocation, not an offset into one shared buffer. The batched payload
|
||||||
|
// below cannot express that: it carries ONE index-buffer view for the whole batch and
|
||||||
|
// turns each pointer into a firstIndex relative to it. Replay the sub-draws through
|
||||||
|
// the single-draw entry point instead - it snapshots each client range into its own
|
||||||
|
// transient slice, which is exactly what the unrolled draws this must match do.
|
||||||
|
// (The batch used to be built this way; the shared-view rewrite that added
|
||||||
|
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||||
|
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||||
|
// declined the whole batch and painted nothing.)
|
||||||
|
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
|
basevertex != nullptr ? basevertex[i] : 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiDrawIndexedCmd payload{};
|
||||||
|
payload.mode = mode;
|
||||||
|
payload.indexBufferView.indexType = type;
|
||||||
|
|
||||||
|
// Loop-invariant: the index type is fixed for the whole multi-draw, so resolve
|
||||||
|
// its byte size once instead of twice per sub-draw (a cross-TU switch that
|
||||||
|
// showed up in per-frame profiles of sodium-style 132x32 multi-draws). Index
|
||||||
|
// sizes are 1/2/4, so the per-sub-draw offset division below reduces to a
|
||||||
|
// shift - the hardware divide was the hottest instruction of this loop.
|
||||||
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
|
if (indexSize == 0) {
|
||||||
|
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||||
|
|
||||||
|
// TODO: allocate draw cmd buf elsewhere
|
||||||
|
static Vector<DrawIndexedCmdParam> params;
|
||||||
|
params.clear();
|
||||||
|
params.resize(drawcount);
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
|
||||||
|
|
||||||
|
payload.indexBufferView.indexByteOffset = 0;
|
||||||
|
payload.indexBufferView.indexByteSize =
|
||||||
|
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * indexSize,
|
||||||
|
payload.indexBufferView.indexByteSize);
|
||||||
|
|
||||||
|
auto& param = params[i];
|
||||||
|
|
||||||
|
param.indexCount = count[i];
|
||||||
|
param.instanceCount = 1;
|
||||||
|
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) >> indexSizeShift;
|
||||||
|
param.vertexOffset = basevertex != nullptr ? basevertex[i] : 0;
|
||||||
|
param.firstInstance = 0;
|
||||||
|
}
|
||||||
|
payload.drawCount = drawcount;
|
||||||
|
payload.pParams = params.data();
|
||||||
|
pVulkanRenderer->MultiDrawElements(payload);
|
||||||
|
}
|
||||||
|
|
||||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
GLsizei drawcount) {
|
GLsizei drawcount) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||||
|
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
|
||||||
// Vector<DrawElementCmd> cmds;
|
|
||||||
// cmds.reserve(static_cast<SizeT>(drawcount));
|
|
||||||
// for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
// if (count[i] == 0) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DrawElementCmd payload{};
|
|
||||||
// payload.mode = mode;
|
|
||||||
// payload.firstVertex = 0;
|
|
||||||
// payload.indexCount = count[i];
|
|
||||||
// payload.indexType = type;
|
|
||||||
// payload.indexByteOffset = reinterpret_cast<SizeT>(indices[i]);
|
|
||||||
// cmds.push_back(payload);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (cmds.empty()) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// pVulkanRenderer->MultiDrawElements(cmds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||||
@@ -1445,52 +1097,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
GLsizei drawcount, const GLint* basevertex) {
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||||
MultiDrawIndexedCmd payload{};
|
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
|
||||||
payload.mode = mode;
|
|
||||||
payload.indexBufferView.indexType = type;
|
|
||||||
|
|
||||||
// Loop-invariant: the index type is fixed for the whole multi-draw, so resolve
|
|
||||||
// its byte size once instead of twice per sub-draw (a cross-TU switch that
|
|
||||||
// showed up in per-frame profiles of sodium-style 132x32 multi-draws). Index
|
|
||||||
// sizes are 1/2/4, so the per-sub-draw offset division below reduces to a
|
|
||||||
// shift - the hardware divide was the hottest instruction of this loop.
|
|
||||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
||||||
if (indexSize == 0) {
|
|
||||||
MGLOG_E("MultiDrawElementsBaseVertex skipped: unsupported index type 0x%x", type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
|
||||||
|
|
||||||
// TODO: allocate draw cmd buf elsewhere
|
|
||||||
static Vector<DrawIndexedCmdParam> params;
|
|
||||||
params.clear();
|
|
||||||
params.resize(drawcount);
|
|
||||||
|
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
if (count[i] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
|
|
||||||
|
|
||||||
payload.indexBufferView.indexByteOffset = 0;
|
|
||||||
payload.indexBufferView.indexByteSize =
|
|
||||||
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * indexSize,
|
|
||||||
payload.indexBufferView.indexByteSize);
|
|
||||||
|
|
||||||
auto& param = params[i];
|
|
||||||
|
|
||||||
param.indexCount = count[i];
|
|
||||||
param.instanceCount = 1;
|
|
||||||
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) >> indexSizeShift;
|
|
||||||
param.vertexOffset = basevertex[i];
|
|
||||||
param.firstInstance = 0;
|
|
||||||
}
|
|
||||||
payload.drawCount = drawcount;
|
|
||||||
payload.pParams = params.data();
|
|
||||||
pVulkanRenderer->MultiDrawElements(payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||||
|
|||||||
@@ -97,15 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
|
||||||
GLsizei* length, GLchar* name);
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||||
|
|||||||
@@ -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,19 +59,56 @@ 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;
|
||||||
|
|
||||||
|
struct UpdateAfterBindLimits {
|
||||||
|
Bool enabled = false;
|
||||||
|
Uint32 maxPerStageSamplers = 0;
|
||||||
|
Uint32 maxPerStageUniformBuffers = 0;
|
||||||
|
Uint32 maxPerStageStorageBuffers = 0;
|
||||||
|
Uint32 maxPerStageSampledImages = 0;
|
||||||
|
Uint32 maxPerStageStorageImages = 0;
|
||||||
|
Uint32 maxPerStageResources = 0;
|
||||||
|
Uint32 maxSetSamplers = 0;
|
||||||
|
Uint32 maxSetUniformBuffers = 0;
|
||||||
|
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||||
|
Uint32 maxSetStorageBuffers = 0;
|
||||||
|
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||||
|
Uint32 maxSetSampledImages = 0;
|
||||||
|
Uint32 maxSetStorageImages = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct VkProgramObject {
|
struct VkProgramObject {
|
||||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||||
|
|
||||||
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;
|
||||||
|
// True only when this layout passed every descriptor-indexing feature and
|
||||||
|
// update-after-bind limit gate at reflection time. It controls both the
|
||||||
|
// layout/binding flags and the pool class used by UniformManager.
|
||||||
|
Bool usesUpdateAfterBind = false;
|
||||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||||
Vector<DescriptorBindingKind> bindingKinds;
|
Vector<DescriptorBindingKind> bindingKinds;
|
||||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||||
@@ -75,8 +119,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 +130,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 +142,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 +167,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,7 +208,16 @@ 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;
|
||||||
|
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
activeBindings = std::move(other.activeBindings);
|
activeBindings = std::move(other.activeBindings);
|
||||||
@@ -136,6 +235,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 +245,17 @@ 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;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
|
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.usesUpdateAfterBind = false;
|
||||||
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 +263,10 @@ 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.writesViewportIndexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
@@ -167,7 +277,9 @@ 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;
|
||||||
|
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
activeBindings = std::move(other.activeBindings);
|
activeBindings = std::move(other.activeBindings);
|
||||||
@@ -185,6 +297,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 +307,17 @@ 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;
|
||||||
|
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||||
|
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.usesUpdateAfterBind = false;
|
||||||
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 +325,10 @@ 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.writesViewportIndexBuiltin = false;
|
||||||
|
other.needsPassthroughTessControl = false;
|
||||||
|
other.passthroughTessControlEmulatable = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -233,6 +356,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
modules.clear();
|
modules.clear();
|
||||||
stages.clear();
|
stages.clear();
|
||||||
|
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,21 +372,62 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
||||||
Bool shaderDrawParametersEnabled = false,
|
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
||||||
Bool unformattedFloatStorageImagesEnabled = false)
|
// subgroup properties) so lowering can never disagree with the advertised
|
||||||
|
// capabilities. Native subgroup operations always execute natively; the two
|
||||||
|
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||||
|
// on opted-in devices with no subgroup support at all.
|
||||||
|
struct SubgroupLoweringPolicy {
|
||||||
|
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||||
|
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||||
|
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
||||||
|
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||||
|
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
||||||
|
Uint32 nativeSubgroupSize = 0;
|
||||||
|
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
||||||
|
// workgroup needs more subgroups than this cannot request the flag.
|
||||||
|
Uint32 maxComputeWorkgroupSubgroups = 0;
|
||||||
|
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
||||||
|
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
||||||
|
Uint32 maxComputeSharedMemoryBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||||
|
Bool shaderDrawParametersEnabled,
|
||||||
|
Bool unformattedFloatStorageImagesEnabled,
|
||||||
|
Bool enableSpirvValidation,
|
||||||
|
UpdateAfterBindLimits updateAfterBindLimits,
|
||||||
|
SubgroupLoweringPolicy subgroupPolicy)
|
||||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||||
|
m_enableSpirvValidation(enableSpirvValidation),
|
||||||
|
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||||
|
m_subgroupPolicy(subgroupPolicy) {
|
||||||
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 +456,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 +502,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 +527,28 @@ 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;
|
||||||
|
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||||
|
// originate from a ProgramLinkTask.
|
||||||
|
Bool m_enableSpirvValidation = false;
|
||||||
|
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||||
|
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||||
|
// own counts would exceed the update-after-bind budget.
|
||||||
|
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||||
|
SubgroupLoweringPolicy m_subgroupPolicy{};
|
||||||
|
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||||
|
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||||
|
Uint32 m_defaultFramebufferHeight = 0;
|
||||||
mutable ProgramLookupCache m_lastLookup;
|
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
@@ -26,9 +26,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
public:
|
public:
|
||||||
struct SamplerBindingOverride {
|
struct SamplerBindingOverride {
|
||||||
Uint32 binding = 0;
|
Uint32 binding = 0;
|
||||||
|
Uint32 element = 0;
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
VkImageView imageView = VK_NULL_HANDLE;
|
VkImageView imageView = VK_NULL_HANDLE;
|
||||||
|
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
Bool forceNearestFiltering = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SamplerImageFeedbackBinding {
|
||||||
|
Uint32 samplerBinding = 0;
|
||||||
|
Uint32 samplerElement = 0;
|
||||||
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
|
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||||
@@ -53,10 +64,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;
|
||||||
@@ -76,6 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||||
|
Bool CollectSamplerImageFeedback(
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||||
|
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||||
|
GLint imageLevel, GLenum imageAccess);
|
||||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||||
// every input of every combined-image-sampler resolution is unchanged since the
|
// every input of every combined-image-sampler resolution is unchanged since the
|
||||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||||
@@ -88,7 +108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||||
Bool samplerDescriptorsUnchangedHint = false);
|
Bool samplerDescriptorsUnchangedHint = false,
|
||||||
|
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||||
|
|
||||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||||
@@ -111,6 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||||
Uint32 maxSets = 0;
|
Uint32 maxSets = 0;
|
||||||
Uint32 allocatedSets = 0;
|
Uint32 allocatedSets = 0;
|
||||||
|
Bool updateAfterBind = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A cached descriptor set together with the pool it was allocated from, so a
|
// A cached descriptor set together with the pool it was allocated from, so a
|
||||||
@@ -143,8 +165,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 +175,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 {
|
||||||
@@ -201,8 +242,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||||
const Vector<Uint32>& dynamicOffsets);
|
const Vector<Uint32>& dynamicOffsets);
|
||||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||||
VkResult AllocateDescriptorSetsFromActivePool(
|
VkResult AllocateDescriptorSetsFromActivePool(
|
||||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||||
@@ -319,8 +360,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||||
|
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||||
|
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||||
struct SamplerResolveMemo {
|
struct SamplerResolveMemo {
|
||||||
|
Uint64 infoProgramLifetimeId = 0;
|
||||||
Uint64 samplerLifetimeId = 0;
|
Uint64 samplerLifetimeId = 0;
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
VkSampler sampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
@@ -341,9 +385,31 @@ 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;
|
||||||
};
|
};
|
||||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||||
|
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
|
||||||
|
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
|
||||||
|
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
|
||||||
|
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
|
||||||
|
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
|
||||||
|
// an entry's *Valid flags on raises this mark first, so entries at or above it are
|
||||||
|
// provably still in their constructed (all-invalid) state and clearing them is a
|
||||||
|
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
|
||||||
|
mutable Uint32 m_samplerResolveMemoHighWater = 0;
|
||||||
|
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
|
||||||
|
if (binding >= m_samplerResolveMemoHighWater) {
|
||||||
|
m_samplerResolveMemoHighWater = binding + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -33,14 +33,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||||
|
|
||||||
// The buffer's heap address is an identity component of the key: a freed
|
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||||
// buffer's reused address can alias an old cache entry, but only under a
|
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||||
// byte-identical attribute layout - and the entry payload is a pure function
|
// hash. An address is recycled by the allocator, so a deleted-and-recreated
|
||||||
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
// buffer reproduces it; combined with a byte-identical attribute layout that
|
||||||
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
// reproduces the WHOLE content hash, and the hash is what
|
||||||
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
|
||||||
// aging sweep bounds that.
|
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
|
||||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
// slice was bound for its successor's draw, which is how a transform-feedback
|
||||||
|
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||||
|
// test's positions) instead of its own.
|
||||||
|
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||||
|
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,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);
|
||||||
@@ -121,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),
|
||||||
@@ -131,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);
|
||||||
@@ -142,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);
|
||||||
@@ -165,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;
|
||||||
@@ -271,8 +287,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||||
it = m_cache.erase(it);
|
it = m_cache.erase(it);
|
||||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||||
// address may be reused by a future insert.
|
// address may be reused by a future insert. Advance through the
|
||||||
++m_evictionEpoch;
|
// process-wide source so the value stays unique across factory
|
||||||
|
// instances (see the member comment).
|
||||||
|
m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||||
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
// unsupported mask) - NO buffer identities. `hash` mixes each bound
|
||||||
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
|
||||||
// pipelines on that minted one VkPipeline per chunk section for an
|
// identity per buffer; keying pipelines on that minted one VkPipeline per
|
||||||
// identical layout, defeating pipeline reuse and the per-draw memo.
|
// chunk section for an identical layout, defeating pipeline reuse and the
|
||||||
// Pipelines depend only on the layout, so they key on this instead.
|
// per-draw memo. Pipelines depend only on the layout, so they key on this
|
||||||
|
// instead.
|
||||||
HashType layoutHash = 0;
|
HashType layoutHash = 0;
|
||||||
// Frame boundary of the last cache hit; entries idle past the
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||||
@@ -86,8 +87,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||||
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
||||||
// minting fresh keys; without eviction the map grows for the whole session.
|
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
||||||
|
// not by luck: a recreated buffer can no longer land back on its dead
|
||||||
|
// predecessor's key. Without eviction the map grows for the whole session.
|
||||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||||
// and the draw path's entry reference never spans a frame boundary, so
|
// and the draw path's entry reference never spans a frame boundary, so
|
||||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||||
@@ -108,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;
|
||||||
@@ -120,7 +125,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// construction); a memo is honored only while its recorded epoch
|
// construction); a memo is honored only while its recorded epoch
|
||||||
// matches, so an evicted entry can never be dereferenced through a
|
// matches, so an evicted entry can never be dereferenced through a
|
||||||
// stale memo.
|
// stale memo.
|
||||||
Uint64 m_evictionEpoch = 1;
|
//
|
||||||
|
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||||
|
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
||||||
|
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||||
|
// factory restarting at a dead factory's epoch value would honor its
|
||||||
|
// dangling entry pointers. The constructor takes a value strictly greater
|
||||||
|
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||||
|
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||||
|
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||||
|
static inline Uint64 s_evictionEpochSource = 0;
|
||||||
|
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -166,7 +166,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||||
dst.mask |= src.mask;
|
dst.mask |= src.mask;
|
||||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
|
// The whole colour story travels together (same rule as
|
||||||
|
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||||
|
// payload carries its value in colorInt/colorUint and its branch selector
|
||||||
|
// in colorEncoding - dropping them here would leave the pending clear
|
||||||
|
// reading as an all-zero float one.
|
||||||
dst.color = src.color;
|
dst.color = src.color;
|
||||||
|
dst.colorEncoding = src.colorEncoding;
|
||||||
|
dst.colorInt = src.colorInt;
|
||||||
|
dst.colorUint = src.colorUint;
|
||||||
}
|
}
|
||||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
dst.depth = src.depth;
|
dst.depth = src.depth;
|
||||||
|
|||||||
@@ -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());
|
||||||
@@ -831,6 +831,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
||||||
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
||||||
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
||||||
|
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
||||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||||
@@ -855,6 +856,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
||||||
m_rpFastValid = true;
|
m_rpFastValid = true;
|
||||||
m_rpFastFbo = &fbo;
|
m_rpFastFbo = &fbo;
|
||||||
|
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
||||||
m_rpFastFboVersion = fbo.GetObjectVersion();
|
m_rpFastFboVersion = fbo.GetObjectVersion();
|
||||||
m_rpFastSwapchainIndex = swapchainImageIndex;
|
m_rpFastSwapchainIndex = swapchainImageIndex;
|
||||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||||
@@ -929,7 +931,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 +1107,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 +1163,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 +1225,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);
|
||||||
}
|
}
|
||||||
@@ -1507,7 +1509,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ClearAttachmentPayload clearPayload{};
|
ClearAttachmentPayload clearPayload{};
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||||
if (pending.hasInlinePayload) {
|
if (pending.hasInlinePayload) {
|
||||||
clearPayload = pending.inlinePayload;
|
// The inline payload was snapshotted when the entry was CREATED, but the
|
||||||
|
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
||||||
|
// glClear would replay the creation-time value and drop the new one (the
|
||||||
|
// texture path below is immune because it re-reads the live payload).
|
||||||
|
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
||||||
|
// pending clear, fall back to the snapshot only when none is queued.
|
||||||
|
if (s_renderPassManager != nullptr &&
|
||||||
|
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
||||||
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
||||||
|
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
||||||
|
3) {
|
||||||
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||||
|
ForceOpaqueClearAlpha(clearPayload);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearPayload = pending.inlinePayload;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pending.key.texture == nullptr ||
|
if (pending.key.texture == nullptr ||
|
||||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -253,6 +289,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||||
Bool m_rpFastValid = false;
|
Bool m_rpFastValid = false;
|
||||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||||
|
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||||
|
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||||
|
// the same number of version bumps would otherwise compare equal (both count
|
||||||
|
// from 0), serving the dead framebuffer's pass to the new object.
|
||||||
|
Uint64 m_rpFastFboLifetimeId = 0;
|
||||||
Uint16 m_rpFastFboVersion = 0;
|
Uint16 m_rpFastFboVersion = 0;
|
||||||
Uint32 m_rpFastSwapchainIndex = 0;
|
Uint32 m_rpFastSwapchainIndex = 0;
|
||||||
Uint64 m_rpFastTexEpoch = 0;
|
Uint64 m_rpFastTexEpoch = 0;
|
||||||
@@ -315,26 +356,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.
|
||||||
@@ -1295,6 +1291,158 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
SamplerNumericDomain numericDomain,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask,
|
||||||
|
SampledTextureSnapshot& outSnapshot) {
|
||||||
|
outSnapshot = {};
|
||||||
|
TextureResource* source = SyncTextureAndGetDescriptor(texture);
|
||||||
|
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||||
|
source->sampledLevelCount == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
|
||||||
|
if (sampledFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
|
||||||
|
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sampledFormat != source->format &&
|
||||||
|
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
|
||||||
|
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
switch (source->viewType) {
|
||||||
|
case VK_IMAGE_VIEW_TYPE_1D:
|
||||||
|
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||||
|
imageType = VK_IMAGE_TYPE_1D;
|
||||||
|
break;
|
||||||
|
case VK_IMAGE_VIEW_TYPE_3D:
|
||||||
|
imageType = VK_IMAGE_TYPE_3D;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureResource snapshot{};
|
||||||
|
VkImageCreateInfo imageInfo{};
|
||||||
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
imageInfo.flags = source->imageCreateFlags;
|
||||||
|
imageInfo.imageType = imageType;
|
||||||
|
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
|
||||||
|
imageInfo.mipLevels = source->mipLevels;
|
||||||
|
imageInfo.arrayLayers = source->arrayLayers;
|
||||||
|
imageInfo.format = source->format;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
|
||||||
|
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
|
||||||
|
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
|
||||||
|
Vector<VkFormat> viewFormats;
|
||||||
|
VkImageFormatListCreateInfo formatListInfo{};
|
||||||
|
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
|
viewFormats.push_back(source->format);
|
||||||
|
if (sampledFormat != source->format) {
|
||||||
|
viewFormats.push_back(sampledFormat);
|
||||||
|
}
|
||||||
|
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||||
|
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||||
|
formatListInfo.pViewFormats = viewFormats.data();
|
||||||
|
imageInfo.pNext = &formatListInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
VmaAllocationCreateInfo allocationInfo{};
|
||||||
|
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||||
|
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||||
|
const VkResult createResult =
|
||||||
|
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
|
||||||
|
if (createResult != VK_SUCCESS) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
|
||||||
|
texture.GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.extent = source->extent;
|
||||||
|
snapshot.depth = source->depth;
|
||||||
|
snapshot.arrayLayers = source->arrayLayers;
|
||||||
|
snapshot.mipLevels = source->mipLevels;
|
||||||
|
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
|
||||||
|
snapshot.sampledLevelCount = source->sampledLevelCount;
|
||||||
|
snapshot.format = source->format;
|
||||||
|
snapshot.aspect = source->aspect;
|
||||||
|
snapshot.viewType = source->viewType;
|
||||||
|
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
snapshot.imageCreateFlags = imageInfo.flags;
|
||||||
|
snapshot.usageFlags = imageInfo.usage;
|
||||||
|
|
||||||
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
|
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||||
|
const VkImageAspectFlags sampledAspect =
|
||||||
|
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
|
||||||
|
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
|
||||||
|
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
|
||||||
|
snapshot.arrayLayers, &sampledComponents);
|
||||||
|
if (snapshot.sampledView == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
VkAccessFlags sourceAccessMask = 0;
|
||||||
|
const VkImageLayout sourceLayout = source->layout;
|
||||||
|
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
|
||||||
|
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
|
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
|
||||||
|
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
|
||||||
|
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
||||||
|
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||||
|
snapshot.sampledLevelCount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<VkImageCopy> copyRegions;
|
||||||
|
copyRegions.reserve(snapshot.sampledLevelCount);
|
||||||
|
for (Uint32 level = snapshot.sampledBaseMipLevel;
|
||||||
|
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
|
||||||
|
VkImageCopy copy{};
|
||||||
|
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
|
||||||
|
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
|
||||||
|
copy.extent = {std::max(source->extent.width >> level, 1u),
|
||||||
|
std::max(source->extent.height >> level, 1u),
|
||||||
|
std::max(source->depth >> level, 1u)};
|
||||||
|
copyRegions.push_back(copy);
|
||||||
|
}
|
||||||
|
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
||||||
|
|
||||||
|
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
|
||||||
|
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
|
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
|
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
||||||
|
snapshot.sampledLevelCount) ||
|
||||||
|
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
|
||||||
|
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
|
source->aspect, 0, source->mipLevels)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StampResourceRecordingUse(*source);
|
||||||
|
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
|
||||||
|
DeferResourceRelease(Move(snapshot));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||||
}
|
}
|
||||||
@@ -1354,8 +1502,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 +1528,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 +1727,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 +1956,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 +1974,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());
|
||||||
@@ -1839,6 +1993,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
texture.GetExternalIndex(),
|
texture.GetExternalIndex(),
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||||
|
// The preserved image was written by GPU work that may still be in flight
|
||||||
|
// (preserve requires layout != UNDEFINED); park it on the deferred ring
|
||||||
|
// like every other destruction path instead of letting the unique_ptr
|
||||||
|
// destroy it synchronously under the GPU.
|
||||||
|
if (preservedResource) {
|
||||||
|
DeferResourceRelease(Move(*preservedResource));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1852,13 +2013,21 @@ 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,
|
||||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||||
resource.image = VK_NULL_HANDLE;
|
resource.image = VK_NULL_HANDLE;
|
||||||
resource.allocation = nullptr;
|
resource.allocation = nullptr;
|
||||||
|
// Same as the probe failure above: the preserved live image must go through
|
||||||
|
// the deferred ring, never a synchronous destructor while frames that
|
||||||
|
// reference it are still in flight.
|
||||||
|
if (preservedResource) {
|
||||||
|
DeferResourceRelease(Move(*preservedResource));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||||
@@ -2238,7 +2407,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 +2594,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 +2771,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 +2875,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 +3030,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,6 +310,11 @@ public:
|
|||||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SampledTextureSnapshot {
|
||||||
|
VkImageView imageView = VK_NULL_HANDLE;
|
||||||
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
};
|
||||||
|
|
||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
@@ -343,6 +348,13 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
|
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||||
|
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||||
|
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||||
|
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||||
|
SamplerNumericDomain numericDomain,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask,
|
||||||
|
SampledTextureSnapshot& outSnapshot);
|
||||||
|
|
||||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||||
// generation advances every time the frame command buffer (re)begins
|
// generation advances every time the frame command buffer (re)begins
|
||||||
@@ -379,17 +391,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,
|
||||||
@@ -224,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||||
|
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||||
|
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||||
|
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||||
|
VkExtent2D imageExtent,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||||
|
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||||
|
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||||
|
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||||
|
Uint32 logicalHeight,
|
||||||
|
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||||
|
SizeT texelSize, Uint8* outPixels);
|
||||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
GLenum destinationType, SizeT destinationRowStride,
|
GLenum destinationType, SizeT destinationRowStride,
|
||||||
@@ -293,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||||
// honored rather than accepted-and-ignored.
|
// honored rather than accepted-and-ignored.
|
||||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||||
|
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||||
|
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||||
|
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||||
|
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||||
|
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||||
|
}
|
||||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||||
@@ -327,6 +350,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool RecreateSwapchain();
|
Bool RecreateSwapchain();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Tiered emission for an already-set-up multi-draw batch (state bound, index
|
||||||
|
// buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
|
||||||
|
// vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
|
||||||
|
// vkCmdDraw(Indexed) loop. Tier eligibility is per-batch (uniform instance
|
||||||
|
// state for tier 1, firstInstance/feature legality for tier 2); every tier
|
||||||
|
// consumes the same param span, so contiguous-run merging done by the caller
|
||||||
|
// benefits all of them.
|
||||||
|
void EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams, Uint32 drawCount);
|
||||||
|
void EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount);
|
||||||
|
|
||||||
struct BlitUniformData {
|
struct BlitUniformData {
|
||||||
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
|
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
|
||||||
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
|
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
|
||||||
@@ -353,6 +386,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;
|
||||||
@@ -435,15 +493,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
|
||||||
@@ -477,10 +533,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_indexTypeUint8ExtensionEnabled = false;
|
Bool m_indexTypeUint8ExtensionEnabled = false;
|
||||||
Bool m_logicOpFeatureEnabled = false;
|
Bool m_logicOpFeatureEnabled = false;
|
||||||
Bool m_multiDrawIndirectFeatureEnabled = false;
|
Bool m_multiDrawIndirectFeatureEnabled = false;
|
||||||
|
// drawIndirectFirstInstance gates indirect commands whose firstInstance != 0;
|
||||||
|
// cached at device creation because the tier-2 multi-draw path (a transient
|
||||||
|
// VkDrawIndexedIndirectCommand array) is illegal for such a sub-draw without it.
|
||||||
|
Bool m_drawIndirectFirstInstanceFeatureEnabled = false;
|
||||||
|
// VK_EXT_multi_draw: native batched submission for the CPU-side glMultiDraw*
|
||||||
|
// families (tier 1 of the multi-draw dispatch).
|
||||||
|
Bool m_multiDrawExtensionEnabled = false;
|
||||||
|
Uint32 m_maxMultiDrawCount = 0;
|
||||||
|
// Multi-draw dispatch tiers, resolved once at device creation from device support
|
||||||
|
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE (a preference, never a demand):
|
||||||
|
// tier 1 (ext): one vkCmdDrawMulti(Indexed)EXT - m_multiDrawAllowExt
|
||||||
|
// tier 2 (indirect): one vkCmdDraw(Indexed)Indirect batch - m_multiDrawAllowIndirect
|
||||||
|
// tier 3 (unroll): one vkCmdDraw(Indexed) per sub-draw - always available
|
||||||
|
// m_multiDrawForceUnrollIndirect additionally forces the GPU-parameter
|
||||||
|
// glMultiDraw*Indirect paths onto their per-command loop (mode=unroll only).
|
||||||
|
Bool m_multiDrawAllowExt = false;
|
||||||
|
Bool m_multiDrawAllowIndirect = false;
|
||||||
|
Bool m_multiDrawForceUnrollIndirect = false;
|
||||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
|
// Native subgroup topology, queried at device creation for the compute-module
|
||||||
|
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
||||||
|
// stage flag; 0 / false when the device has no usable compute subgroups or
|
||||||
|
// MOBILEGL_DISABLE_SUBGROUP forced them off.
|
||||||
|
Uint32 m_nativeSubgroupSize = 0;
|
||||||
|
Bool m_nativeSubgroupSupported = false;
|
||||||
|
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
||||||
|
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
|
||||||
|
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
|
||||||
|
Uint32 m_maxComputeWorkgroupSubgroups = 0;
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
|
// Set only after descriptor-indexing feature AND property queries prove that
|
||||||
|
// update-after-bind is legal for every descriptor category this renderer emits.
|
||||||
|
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||||
// drive a runtime fallback when the device lacks them.
|
// drive a runtime fallback when the device lacks them.
|
||||||
@@ -491,6 +578,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
|
||||||
@@ -508,6 +601,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
||||||
Uint32 stride);
|
Uint32 stride);
|
||||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||||
|
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
|
||||||
|
// (and its multiDraw feature) is enabled; null otherwise.
|
||||||
|
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
||||||
|
static inline PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||||
|
|
||||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||||
Bool m_transformFeedbackFeatureEnabled = false;
|
Bool m_transformFeedbackFeatureEnabled = false;
|
||||||
@@ -659,7 +756,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
||||||
Bool m_baseTransformFlagsKeyValid = false;
|
Bool m_baseTransformFlagsKeyValid = false;
|
||||||
Uint32 m_baseTransformFlagsCache = 0;
|
Uint32 m_baseTransformFlagsCache = 0;
|
||||||
Uint32 GetBaseTransformFlagsRaw();
|
// isDefaultFbo must be the default-ness of the CURRENTLY bound draw framebuffer;
|
||||||
|
// every caller already has it in hand from its own guards.
|
||||||
|
Uint32 GetBaseTransformFlagsRaw(Bool isDefaultFbo);
|
||||||
// Drops every memoized pipeline handle. Required at command-buffer
|
// Drops every memoized pipeline handle. Required at command-buffer
|
||||||
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||||
// the cached pipeline-state hash: the same boundaries can retire the GL
|
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||||
@@ -708,9 +807,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 m_lastLodProgramVersion = 0;
|
Uint32 m_lastLodProgramVersion = 0;
|
||||||
Uint64 m_lastLodBindGeneration = 0;
|
Uint64 m_lastLodBindGeneration = 0;
|
||||||
Uint64 m_lastLodParamsSum = 0;
|
Uint64 m_lastLodParamsSum = 0;
|
||||||
|
// Sampling-resolution generation at probe time. The probe reads the effective
|
||||||
|
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
|
||||||
|
// the params-version sum above never moves for them.
|
||||||
|
Uint64 m_lastLodSamplingGeneration = 0;
|
||||||
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
|
||||||
@@ -724,8 +840,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 programLifetimeId = 0;
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 programVersion = 0;
|
Uint32 programVersion = 0;
|
||||||
const void* vao = nullptr;
|
const void* vao = nullptr;
|
||||||
|
// Same rule as VaoDrawMemo::vaoLifetimeId: (address, config version) is not an
|
||||||
|
// identity, because a recycled address can arrive carrying a config version
|
||||||
|
// the dead VAO also had (two mutations to configure one attribute is the
|
||||||
|
// common shape), and "the VAO did not move" would then skip the layout
|
||||||
|
// re-resolve for a different VAO.
|
||||||
|
Uint64 vaoLifetimeId = 0;
|
||||||
Uint32 vaoConfigVersion = 0;
|
Uint32 vaoConfigVersion = 0;
|
||||||
const void* drawFbo = nullptr;
|
const void* drawFbo = nullptr;
|
||||||
|
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||||
|
// deleted FBO recycled at the same address with the same fresh version
|
||||||
|
// count would otherwise compare equal (same ABA as the render-pass
|
||||||
|
// manager's fast-path memo).
|
||||||
|
Uint64 drawFboLifetimeId = 0;
|
||||||
Uint16 fboVersion = 0;
|
Uint16 fboVersion = 0;
|
||||||
Bool drawFboIsDefault = false;
|
Bool drawFboIsDefault = false;
|
||||||
Uint renderStateVersion = 0;
|
Uint renderStateVersion = 0;
|
||||||
@@ -749,6 +876,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
|
||||||
@@ -816,6 +948,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// already sampleable.
|
// already sampleable.
|
||||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
|
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
|
||||||
|
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||||
@@ -942,6 +1076,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||||
VkDeviceSize indexSliceOffset = 0;
|
VkDeviceSize indexSliceOffset = 0;
|
||||||
Uint64 indexFrameSerial = 0;
|
Uint64 indexFrameSerial = 0;
|
||||||
|
// The EBO carried a host map when the slice was recorded - the mirror of
|
||||||
|
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
|
||||||
|
// persistent map mutates its shadow with no API call and no epoch bump, so
|
||||||
|
// the one-compare rescue must decline and re-run the acquire, whose
|
||||||
|
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
|
||||||
|
// is already covered: AcquirePersistentMap bumps the slice epoch for the
|
||||||
|
// request itself, adopted or declined.
|
||||||
|
Bool indexBufferMapped = false;
|
||||||
|
|
||||||
// Bound per draw (first bindingCount elements).
|
// Bound per draw (first bindingCount elements).
|
||||||
VkBuffer vkBuffers[kMaxBindings] = {};
|
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||||
@@ -953,19 +1095,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
|
const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
|
||||||
Uint64 sliceEpochs[kMaxBindings] = {};
|
Uint64 sliceEpochs[kMaxBindings] = {};
|
||||||
};
|
};
|
||||||
// One direct-mapped slot of the per-VAO draw-memo table below. The key is a
|
// One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to
|
||||||
// lookup hint only - a slot is never dereferenced through vaoKey; every fact it
|
// the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone
|
||||||
// carries is validated against live state before use:
|
// only picks the slot, and the never-reused lifetime id is what proves the slot
|
||||||
|
// is THIS VAO's, so the successor allocated onto a destroyed VAO's address
|
||||||
|
// always misses. That identity check is load-bearing and the content-hash
|
||||||
|
// validations below do NOT stand in for it - a recycled address under a
|
||||||
|
// byte-identical configuration reproduces the content hash exactly, which is
|
||||||
|
// how a destroyed VAO's resolved bindings were once handed to its successor's
|
||||||
|
// draw. The slot is still never dereferenced through vaoKey, and every fact it
|
||||||
|
// carries is still validated against live state before use:
|
||||||
// - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE
|
// - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE
|
||||||
// VAO's own hash memo (which the VAO's config version guards), so a config
|
// VAO's own hash memo (which the VAO's config version guards), so a config
|
||||||
// change, a buffer rebind, or a recycled VAO address with a different
|
// change or a buffer rebind misses even for the same object.
|
||||||
// configuration all miss. A recycled address with a byte-identical
|
|
||||||
// configuration AND identical bound buffers reproduces the content hash, and
|
|
||||||
// then the facts are correct by construction (they are a pure function of it).
|
|
||||||
// - bindings revalidates per draw exactly as before (frame serial, content
|
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||||
// hash, per-binding live buffer pointers and slice epochs).
|
// hash, per-binding live buffer pointers and slice epochs).
|
||||||
struct alignas(64) VaoDrawMemo {
|
struct alignas(64) VaoDrawMemo {
|
||||||
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||||
|
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||||
|
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||||
|
// back by the next glGenVertexArrays-shaped allocation, and the successor then
|
||||||
|
// matched this slot and inherited the dead object's memos. Both stated
|
||||||
|
// defences failed with it, because both reduce to the content hash and the
|
||||||
|
// content hash's buffer-identity component was itself a recycled heap address.
|
||||||
|
Uint64 vaoLifetimeId = 0;
|
||||||
// The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two
|
// The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two
|
||||||
// layout facts below were derived from; 0 while nothing valid is stored.
|
// layout facts below were derived from; 0 while nothing valid is stored.
|
||||||
Uint64 contentHash = 0;
|
Uint64 contentHash = 0;
|
||||||
@@ -1024,11 +1177,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
FrameContext::FrameData& frame,
|
FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
||||||
|
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
||||||
|
// storage descriptor continues to name the application texture.
|
||||||
|
Bool PrepareSamplerImageFeedbackSnapshots(
|
||||||
|
FrameContext::FrameData& frame,
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
VkPipelineStageFlags consumerShaderStageMask);
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -1069,6 +1245,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,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
|
||||||
|
// shared by capability advertisement (BackendObject) and module lowering
|
||||||
|
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
|
||||||
|
//
|
||||||
|
// Native subgroups are the implementation whenever the device has them, whatever
|
||||||
|
// their width - subgroup operations execute on the hardware paths they were made
|
||||||
|
// for. Module-level repairs keep the GL contract intact around them:
|
||||||
|
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
||||||
|
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
||||||
|
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
|
||||||
|
// reusing that scratch, when explicitly enabled;
|
||||||
|
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
||||||
|
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
||||||
|
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
||||||
|
// devices with no subgroup support at all, and only when the user opts in with
|
||||||
|
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
|
||||||
|
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
|
||||||
|
inline constexpr Uint32 kEmulatedSubgroupFeatures =
|
||||||
|
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
|
||||||
|
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
||||||
|
|
||||||
|
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
||||||
|
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
||||||
|
!MG_Config::Features.DisableSubgroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
||||||
|
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
||||||
|
// grows one under-declared array; every other module passes through untouched.
|
||||||
|
return MG_Config::Features.FixIterationRPSubgroupScratch !=
|
||||||
|
MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldFixIterationRPBarrier() {
|
||||||
|
return MG_Config::Features.IterationRPFixBarrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool ShouldDeriveNumSubgroups() {
|
||||||
|
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
|
||||||
|
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
||||||
|
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
||||||
|
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
||||||
|
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "GL_Buffer.h"
|
#include "GL_Buffer.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
#include "../Texture/GL_Texture.h"
|
#include "../Texture/GL_Texture.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
#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/MGToGL/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||||
|
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -30,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
NamedBufferData,
|
NamedBufferData,
|
||||||
NamedBufferSubData,
|
NamedBufferSubData,
|
||||||
CopyNamedBufferSubData,
|
CopyNamedBufferSubData,
|
||||||
|
ClearBufferData,
|
||||||
|
ClearBufferSubData,
|
||||||
ClearNamedBufferData,
|
ClearNamedBufferData,
|
||||||
ClearNamedBufferSubData,
|
ClearNamedBufferSubData,
|
||||||
MapBufferRange,
|
MapBufferRange,
|
||||||
@@ -64,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return "NamedBufferSubData";
|
return "NamedBufferSubData";
|
||||||
case BufferOp::CopyNamedBufferSubData:
|
case BufferOp::CopyNamedBufferSubData:
|
||||||
return "CopyNamedBufferSubData";
|
return "CopyNamedBufferSubData";
|
||||||
|
case BufferOp::ClearBufferData:
|
||||||
|
return "ClearBufferData";
|
||||||
|
case BufferOp::ClearBufferSubData:
|
||||||
|
return "ClearBufferSubData";
|
||||||
case BufferOp::ClearNamedBufferData:
|
case BufferOp::ClearNamedBufferData:
|
||||||
return "ClearNamedBufferData";
|
return "ClearNamedBufferData";
|
||||||
case BufferOp::ClearNamedBufferSubData:
|
case BufferOp::ClearNamedBufferSubData:
|
||||||
@@ -142,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pattern is replicated verbatim, which is only the whole story while the client
|
|
||||||
// layout already matches the internal format - the case every entry point in practice
|
|
||||||
// uses, and the only one the conversion machinery here can express. Say so rather than
|
|
||||||
// quietly writing a differently-sized pattern.
|
|
||||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
|
||||||
if (sourceSize != elementSize) {
|
|
||||||
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
|
||||||
"converting between them is not implemented",
|
|
||||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
|
||||||
}
|
|
||||||
return elementSize;
|
return elementSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
|
||||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
|
||||||
|
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
|
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||||
|
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||||
|
|
||||||
|
Vector<Uint8> zeroInput;
|
||||||
|
const void* inputPixel = data;
|
||||||
|
if (inputPixel == nullptr) {
|
||||||
|
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
|
||||||
|
if (inputSize == 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
"format and type do not describe a source pixel."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
zeroInput.resize(inputSize);
|
||||||
|
inputPixel = zeroInput.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
|
||||||
|
internal, inputFormat, inputType, inputPixel, pattern)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
|
||||||
|
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
|
||||||
|
internalformat)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == nullptr) {
|
||||||
|
// GL defines a null clear value as all zero bits in the destination store, while
|
||||||
|
// retaining the format/type validation above.
|
||||||
|
pattern.assign(patternSize, 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||||
|
GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
|
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||||
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
||||||
if (patternSize == 0) return;
|
if (patternSize == 0) return;
|
||||||
|
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, op);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
||||||
if (size == 0) return;
|
if (size == 0) return;
|
||||||
|
|
||||||
Vector<Uint8> clearData(static_cast<SizeT>(size));
|
Vector<Uint8> pattern;
|
||||||
if (data) {
|
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
|
||||||
const auto* pattern = static_cast<const Uint8*>(data);
|
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
|
||||||
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
|
static_cast<SizeT>(size));
|
||||||
Memcpy(clearData.data() + at, pattern, patternSize);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Memset(clearData.data(), 0, clearData.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& GetBufferBindingSlot(BufferTarget target) {
|
auto& GetBufferBindingSlot(BufferTarget target) {
|
||||||
@@ -861,6 +891,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
Range1D mappedRange = bufferObject->GetMappedRange();
|
||||||
auto mappingAccess = bufferObject->GetMappingAccess();
|
auto mappingAccess = bufferObject->GetMappingAccess();
|
||||||
|
// GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open
|
||||||
|
// intersection test. There used to be a second test below this one asking only
|
||||||
|
// `offset + size >= mappedRange.start`, which rejects every write that starts
|
||||||
|
// before a mapped tail as well - it made a legal disjoint glBufferSubData fail.
|
||||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
|
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
|
||||||
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
|
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -871,18 +905,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent)) {
|
|
||||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
|
||||||
if (offset + size >= mappedRange.start) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
|
|
||||||
"Cannot modify a mapped buffer object unless it was "
|
|
||||||
"mapped with GL_MAP_PERSISTENT_BIT."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
|
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,6 +1035,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
|
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||||
|
// Error precedence: "no buffer is bound to target" outranks a bad size or bad
|
||||||
|
// flags, so the binding has to be resolved before either is validated.
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -1021,8 +1048,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
|
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
|
||||||
|
|
||||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (bufferObject->IsImmutableStorage()) {
|
if (bufferObject->IsImmutableStorage()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1057,6 +1082,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
|
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||||
|
// Same precedence as BufferStorage_State: the buffer-name error comes first.
|
||||||
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -1065,8 +1094,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
|
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
|
||||||
|
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (bufferObject->IsImmutableStorage()) {
|
if (bufferObject->IsImmutableStorage()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1199,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||||
|
type, data, BufferOp::ClearBufferData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
|
GLenum format, GLenum type, const void* data) {
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||||
|
BufferOp::ClearBufferSubData);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
||||||
if (!bufferObject) return;
|
if (!bufferObject) return;
|
||||||
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||||
type, data, BufferOp::ClearNamedBufferData);
|
type, data, BufferOp::ClearNamedBufferData);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||||
GLenum format, GLenum type, const void* data) {
|
GLenum format, GLenum type, const void* data) {
|
||||||
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
|
||||||
BufferOp::ClearNamedBufferSubData);
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||||
|
BufferOp::ClearNamedBufferSubData);
|
||||||
}
|
}
|
||||||
|
|
||||||
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
|
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
|
||||||
@@ -1486,12 +1530,87 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair.
|
||||||
|
// Every one of them is INVALID_VALUE, and all of them are checked before a single piece
|
||||||
|
// of state is written - a rejected bind must leave the binding point exactly as it was.
|
||||||
|
// They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores
|
||||||
|
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||||
|
// (a NULL buffers array, or a zero entry inside one).
|
||||||
|
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||||
|
const char* funcName, Bool hasBuffer = true) {
|
||||||
|
if (hasBuffer && size <= 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("size ({}) must be greater than zero.", size)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (offset < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must not be negative.", offset)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own
|
||||||
|
// implementation-defined alignment, which glGetIntegerv already answers.
|
||||||
|
GLenum alignmentQuery = GL_NONE;
|
||||||
|
if (target == GL_SHADER_STORAGE_BUFFER) {
|
||||||
|
alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT;
|
||||||
|
} else if (target == GL_UNIFORM_BUFFER) {
|
||||||
|
alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT;
|
||||||
|
}
|
||||||
|
if (alignmentQuery != GL_NONE) {
|
||||||
|
GLint alignment = 0;
|
||||||
|
GetIntegerv(alignmentQuery, &alignment);
|
||||||
|
if (alignment > 0 && (offset % static_cast<GLintptr>(alignment)) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must be a multiple of {} ({}).", offset,
|
||||||
|
MG_Util::ConvertGLEnumToString(alignmentQuery), alignment)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
|
||||||
|
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
|
||||||
|
// queryable alignment pname, which is why it was missing here), and the SIZE only for
|
||||||
|
// transform feedback, whose capture is written in whole 32-bit components. Extending the
|
||||||
|
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
|
||||||
|
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
|
||||||
|
// not required to land on four.
|
||||||
|
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
|
||||||
|
MG_Util::ConvertGLEnumToString(target))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||||
MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
|
MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
|
||||||
MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
|
MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
|
||||||
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;
|
||||||
|
// 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,
|
||||||
@@ -1589,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
|
ClearBufferData_State(target, internalformat, format, type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
|
GLenum type, const void* data) {
|
||||||
|
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||||
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
||||||
}
|
}
|
||||||
@@ -1665,16 +1793,54 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
|
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
|
||||||
// points (with buffer 0 resetting the binding point).
|
// points (with buffer 0 resetting the binding point) - but only AFTER an up-front check
|
||||||
|
// of the whole [first, first + count) range. Looping straight into the single-bind entry
|
||||||
|
// points reports the single-bind INVALID_VALUE for an out-of-range index instead of the
|
||||||
|
// multi-bind INVALID_OPERATION, and binds the in-range prefix before failing.
|
||||||
|
static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) {
|
||||||
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false;
|
||||||
|
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;
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The (offset, size) constraints are the one part of glBindBuffersRange that stays
|
||||||
|
// per-element: ARB_multi_bind checks them separately for each binding point, leaves that
|
||||||
|
// point unchanged on failure, and still applies the remaining elements - which is exactly
|
||||||
|
// what looping into BindBufferRange_State does. Only the [first, first + count) range is
|
||||||
|
// an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array)
|
||||||
|
// reset the binding point through BindBufferBase_State and carry no offset/size to check.
|
||||||
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||||
const GLsizeiptr* sizes) {
|
const GLsizeiptr* sizes) {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||||
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
||||||
GLsizeiptr size);
|
GLsizeiptr size);
|
||||||
|
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||||
|
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
|
GLenum type, const void* data);
|
||||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||||
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||||
GLenum type, const void* data);
|
GLenum type, const void* data);
|
||||||
|
|||||||
@@ -53,18 +53,46 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The GL-visible number of indexed binding points for `target`.
|
||||||
|
SizeT GetBufferBindingPointLimit(BufferTarget target) {
|
||||||
|
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
||||||
|
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
||||||
|
const Int backendCount =
|
||||||
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
|
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||||
|
}
|
||||||
|
if (target == BufferTarget::TransformFeedback) {
|
||||||
|
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||||
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
|
pointCount = std::min<SizeT>(pointCount, 4);
|
||||||
|
}
|
||||||
|
return pointCount;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName) {
|
||||||
|
if (count < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", funcName,
|
||||||
|
"count must be non-negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||||
|
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(pointCount)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/BufferImpl", funcName,
|
||||||
|
std::format("first + count ({} + {}) exceeds the {} indexed binding points of target {}.", first,
|
||||||
|
count, pointCount, MG_Util::ConvertBufferTargetToString(target))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
||||||
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||||
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
|
||||||
const Int backendCount =
|
|
||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
|
||||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
|
||||||
}
|
|
||||||
if (target == BufferTarget::TransformFeedback) {
|
|
||||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
|
||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < pointCount) {
|
if (index < pointCount) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
|
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
|
||||||
Bool ValidateBufferBindingPointTarget(BufferTarget target);
|
Bool ValidateBufferBindingPointTarget(BufferTarget target);
|
||||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
|
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
|
||||||
|
// ARB_multi_bind: glBindBuffersBase/Range validate the whole [first, first + count) range
|
||||||
|
// up front and report INVALID_OPERATION, where a single out-of-range index would be
|
||||||
|
// INVALID_VALUE. Naively looping the single-bind entry points reports the wrong class.
|
||||||
|
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -474,10 +481,63 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
|
// GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT
|
||||||
|
// for its dimension. GetIntegeri_v already floors that at the spec minimum.
|
||||||
|
const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ};
|
||||||
|
for (GLuint dimension = 0; dimension < 3; ++dimension) {
|
||||||
|
GLint maxGroups = 0;
|
||||||
|
GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups);
|
||||||
|
if (numGroups[dimension] > static_cast<GLuint>(std::max(maxGroups, 0))) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " +
|
||||||
|
std::to_string(dimension) + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DispatchComputeIndirect(GLintptr indirect) {
|
void DispatchComputeIndirect(GLintptr indirect) {
|
||||||
|
// Argument and binding validation runs FIRST. Both are properties of the call and of GL
|
||||||
|
// state, so a context whose backend cannot dispatch at all must still report the
|
||||||
|
// argument error the spec names rather than masking every one of them with
|
||||||
|
// "unsupported" - which is what put GL_INVALID_OPERATION where
|
||||||
|
// KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
|
||||||
|
//
|
||||||
|
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
||||||
|
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||||
|
if (indirect < 0 || (indirect % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"indirect must be non-negative and a multiple of 4."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& indirectBuffer =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
|
||||||
|
if (!indirectBuffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ...and the same INVALID_OPERATION covers "the command would source data beyond the end
|
||||||
|
// of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting
|
||||||
|
// at `indirect`.
|
||||||
|
constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32);
|
||||||
|
if (static_cast<SizeT>(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", __func__,
|
||||||
|
std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to "
|
||||||
|
"GL_DISPATCH_INDIRECT_BUFFER.",
|
||||||
|
indirect, indirectBuffer->GetSize())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
||||||
if (!dispatchComputeIndirect) {
|
if (!dispatchComputeIndirect) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -547,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(
|
||||||
@@ -562,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(
|
||||||
@@ -1119,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,24 +969,24 @@ 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_STUB_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_STUB_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_STUB_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_STUB_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)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
||||||
@@ -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)
|
||||||
@@ -1273,7 +1273,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4ivARB, GLenum target, const GL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
|
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
|
||||||
@@ -1381,7 +1381,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3ivARB, const GLint* v) DECLARE_GL_
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, )
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, )
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
|
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t)
|
||||||
@@ -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__);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,9 +106,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
||||||
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
||||||
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
||||||
// widens three-channel formats to four channels to keep them renderable as *multisample*
|
// decides each target's substitution against that target's own probe, and a buffer texture
|
||||||
// storage, and a format that survives only through that substitution is still texture-only
|
// never gets one at all. This is also where the three-channel widening becomes visible to
|
||||||
// on every ordinary target.
|
// the application - a GL_RGB8_SNORM colour attachment on a driver with no renderable
|
||||||
|
// three-channel format answers COMPLETE because the backend stores it as GL_RGBA16F and
|
||||||
|
// recorded FramebufferRenderable in CaveatCaps.
|
||||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
||||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||||
@@ -545,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,
|
||||||
@@ -556,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);
|
||||||
@@ -566,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);
|
||||||
@@ -576,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);
|
||||||
@@ -586,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);
|
||||||
@@ -618,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(
|
||||||
@@ -639,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,
|
||||||
@@ -647,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.
|
||||||
@@ -657,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;
|
||||||
@@ -682,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});
|
||||||
@@ -929,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>
|
||||||
@@ -23,9 +24,15 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#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,
|
||||||
@@ -50,6 +57,23 @@ 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
|
||||||
|
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||||
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
|
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
||||||
|
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
||||||
|
// limits they advertise still have to be legal.
|
||||||
|
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||||
|
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||||
@@ -165,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;
|
||||||
@@ -263,6 +311,104 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_TEXTURE_BINDING_* is per-texture-unit state: glGetIntegerv answers for the
|
||||||
|
// active unit, glGetIntegeri_v answers for unit `index`. Both need the same
|
||||||
|
// pname -> target decode, so it lives here instead of being spelled out twice.
|
||||||
|
bool TryDecodeTextureUnitBindingPname(GLenum pname, TextureTarget& outTarget) {
|
||||||
|
switch (pname) {
|
||||||
|
case GL_TEXTURE_BINDING_1D: outTarget = TextureTarget::Texture1D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_1D_ARRAY: outTarget = TextureTarget::Texture1DArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D: outTarget = TextureTarget::Texture2D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_ARRAY: outTarget = TextureTarget::Texture2DArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: outTarget = TextureTarget::Texture2DMultisample; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY:
|
||||||
|
outTarget = TextureTarget::Texture2DMultisampleArray;
|
||||||
|
return true;
|
||||||
|
case GL_TEXTURE_BINDING_3D: outTarget = TextureTarget::Texture3D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_BUFFER: outTarget = TextureTarget::TextureBuffer; return true;
|
||||||
|
case GL_TEXTURE_BINDING_CUBE_MAP: outTarget = TextureTarget::TextureCubeMap; return true;
|
||||||
|
case GL_TEXTURE_BINDING_CUBE_MAP_ARRAY: outTarget = TextureTarget::TextureCubeMapArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_RECTANGLE: outTarget = TextureTarget::TextureRectangle; return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint QueryTextureBindingOnUnit(Int unit, TextureTarget target) {
|
||||||
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
|
const auto& obj = textureUnit.GetBindingSlot(target).GetBoundObject();
|
||||||
|
return obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint QuerySamplerBindingOnUnit(Int unit) {
|
||||||
|
const auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
|
const auto& sampler = textureUnit.GetSamplerObject();
|
||||||
|
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||||
|
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||||
|
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||||
|
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||||
|
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||||
|
// index with viewport 0's value, which is what
|
||||||
|
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||||
|
Bool IsIndexedViewportQuery(GLenum target) {
|
||||||
|
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||||
|
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||||
|
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||||
|
// getter width funnels through this so they can never disagree with each other.
|
||||||
|
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||||
|
switch (target) {
|
||||||
|
case GL_VIEWPORT: {
|
||||||
|
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||||
|
out[0] = viewport.x();
|
||||||
|
out[1] = viewport.y();
|
||||||
|
out[2] = viewport.z();
|
||||||
|
out[3] = viewport.w();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_SCISSOR_BOX: {
|
||||||
|
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||||
|
out[0] = static_cast<GLfloat>(box.x());
|
||||||
|
out[1] = static_cast<GLfloat>(box.y());
|
||||||
|
out[2] = static_cast<GLfloat>(box.z());
|
||||||
|
out[3] = static_cast<GLfloat>(box.w());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
|
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||||
|
out[0] = range.x();
|
||||||
|
out[1] = range.y();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||||
|
static_cast<Uint32>(target));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
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;
|
||||||
@@ -287,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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,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();
|
||||||
@@ -533,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: {
|
||||||
@@ -659,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:
|
||||||
@@ -670,7 +831,54 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-texture-unit bindings: GL 4.6 core table 23.19 makes every GL_TEXTURE_BINDING_*
|
||||||
|
// and GL_SAMPLER_BINDING indexed by texture unit. Without this they fell through to
|
||||||
|
// the raw backend passthrough at the bottom, which knows nothing about the
|
||||||
|
// frontend's binding state.
|
||||||
|
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||||
|
TryDecodeTextureUnitBindingPname(target, textureBindingTarget) || target == GL_SAMPLER_BINDING) {
|
||||||
|
GLint maxUnits = 0;
|
||||||
|
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
|
||||||
|
maxUnits = std::min<GLint>(maxUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
|
if (index >= static_cast<GLuint>(std::max(maxUnits, 0))) {
|
||||||
|
*data = 0;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture unit index is out of range."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data = target == GL_SAMPLER_BINDING
|
||||||
|
? QuerySamplerBindingOnUnit(static_cast<Int>(index))
|
||||||
|
: QueryTextureBindingOnUnit(static_cast<Int>(index), textureBindingTarget);
|
||||||
|
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
|
||||||
|
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||||
|
case GL_VIEWPORT:
|
||||||
|
case GL_SCISSOR_BOX:
|
||||||
|
case GL_DEPTH_RANGE: {
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
GLfloat values[4] = {};
|
||||||
|
ReadIndexedViewportStateFloat(target, index, values);
|
||||||
|
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||||
|
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||||
|
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
// 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:
|
||||||
@@ -782,6 +990,54 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
getIntegeri(target, index, data);
|
getIntegeri(target, index, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_ARB_viewport_array's typed indexed getters. They were no-op stubs, which left the
|
||||||
|
// caller's output buffer holding whatever was on the stack. The multi-component indexed
|
||||||
|
// rectangles are answered from the frontend's own viewport/scissor/depth-range state, via
|
||||||
|
// the non-indexed getter of the matching type - GL_DEPTH_RANGE is float state, so putting
|
||||||
|
// it through the integer query would round it to 0/1. Everything else MobileGL answers
|
||||||
|
// indexed is scalar integer-domain state, where converting the integer query is exact.
|
||||||
|
void GetFloati_v(GLenum target, GLuint index, GLfloat* data) {
|
||||||
|
if (!data) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsIndexedViewportQuery(target)) {
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||||
|
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||||
|
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||||
|
ReadIndexedViewportStateFloat(target, index, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint ints[4] = {};
|
||||||
|
GetIntegeri_v(target, index, ints);
|
||||||
|
data[0] = static_cast<GLfloat>(ints[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data) {
|
||||||
|
if (!data) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsIndexedViewportQuery(target)) {
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
GLfloat values[4] = {};
|
||||||
|
ReadIndexedViewportStateFloat(target, index, values);
|
||||||
|
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||||
|
for (GLsizei i = 0; i < components; ++i) {
|
||||||
|
data[i] = static_cast<GLdouble>(values[i]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint ints[4] = {};
|
||||||
|
GetIntegeri_v(target, index, ints);
|
||||||
|
data[0] = static_cast<GLdouble>(ints[0]);
|
||||||
|
}
|
||||||
|
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -818,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:
|
||||||
@@ -828,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) {
|
||||||
@@ -958,6 +1233,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-texture-unit bindings: the non-indexed query reports the active unit.
|
||||||
|
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||||
|
TryDecodeTextureUnitBindingPname(pname, textureBindingTarget)) {
|
||||||
|
*params = QueryTextureBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit(), textureBindingTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_ACTIVE_TEXTURE:
|
case GL_ACTIVE_TEXTURE:
|
||||||
*params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
|
*params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
|
||||||
@@ -1069,12 +1351,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case GL_MAX_SHADER_COMPILER_THREADS_KHR:
|
||||||
|
// GL_KHR_parallel_shader_compile (GL_MAX_SHADER_COMPILER_THREADS_ARB is the same
|
||||||
|
// 0x91B0). The number of threads MobileGL's compile pool would actually use, so
|
||||||
|
// an application sizing its own submission batches gets a real answer.
|
||||||
|
//
|
||||||
|
// Zero when asynchronous compilation is off, which is the honest reply and the
|
||||||
|
// one the extension defines for an implementation with no compiler threads: the
|
||||||
|
// extension string is withdrawn in that configuration too, so a conforming
|
||||||
|
// application never reaches this query, and one that asks anyway is told there
|
||||||
|
// are none rather than being handed a thread count nothing will use.
|
||||||
|
*params = MG_Util::Async::AsyncShaderCompileEnabled()
|
||||||
|
? static_cast<GLint>(MG_Util::Async::ShaderCompilePool::Get().GetThreadCount())
|
||||||
|
: 0;
|
||||||
|
return;
|
||||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||||
*params = 0; // debug-group entrypoints are stubbed
|
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||||
|
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||||
|
*params = kFrontendMaxDebugGroupStackDepth;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||||
|
// Size of the message log ring; KHR_debug requires at least 1.
|
||||||
|
*params = kFrontendMaxDebugLoggedMessages;
|
||||||
|
return;
|
||||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||||
*params = 0; // debug-group entrypoints are stubbed
|
*params = 0; // debug-group entrypoints are stubbed
|
||||||
return;
|
return;
|
||||||
@@ -1219,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
|
||||||
@@ -1234,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;
|
||||||
@@ -1251,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;
|
||||||
@@ -1280,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;
|
||||||
@@ -1312,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;
|
||||||
@@ -1322,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
|
||||||
@@ -1342,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;
|
||||||
@@ -1360,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
|
||||||
@@ -1516,13 +1830,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_SAMPLE_MASK_VALUE:
|
case GL_SAMPLE_MASK_VALUE:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||||
return;
|
return;
|
||||||
case GL_SAMPLER_BINDING: {
|
case GL_SAMPLER_BINDING:
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
*params = QuerySamplerBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
const auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& sampler = tu.GetSamplerObject();
|
|
||||||
*params = sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
case GL_SAMPLES:
|
case GL_SAMPLES:
|
||||||
*params = ResolveDrawFramebufferSampleCount();
|
*params = ResolveDrawFramebufferSampleCount();
|
||||||
return;
|
return;
|
||||||
@@ -1618,87 +1928,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_STEREO:
|
case GL_STEREO:
|
||||||
*params = 0; // stereo surfaces are not exposed
|
*params = 0; // stereo surfaces are not exposed
|
||||||
return;
|
return;
|
||||||
case GL_TEXTURE_BINDING_1D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_1D_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1DArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
MGLOG_D("Get GL_TEXTURE_BINDING_2D: %d", *params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisample);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisampleArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_3D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture3D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_BUFFER: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureBuffer);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_CUBE_MAP: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureCubeMap);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_RECTANGLE: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureRectangle);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_COMPRESSION_HINT:
|
case GL_TEXTURE_COMPRESSION_HINT:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
||||||
return;
|
return;
|
||||||
@@ -1816,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();
|
||||||
@@ -1845,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;
|
||||||
@@ -1971,6 +2200,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
||||||
break;
|
break;
|
||||||
|
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||||
|
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||||
|
// state-query conversion rules.
|
||||||
|
*params = static_cast<GLint>(std::min<Uint64>(dynamicParameters.MaxShaderStorageBlockSize,
|
||||||
|
static_cast<Uint64>(INT32_MAX)));
|
||||||
|
break;
|
||||||
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||||
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
|
break;
|
||||||
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
|
// The conformance suite splits this evenly across every advertised binding point and
|
||||||
|
// binds all of them in one glBindBuffersRange
|
||||||
|
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
|
||||||
|
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
|
||||||
|
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
||||||
|
// one counter per binding point.
|
||||||
|
*params = std::max<GLint>(
|
||||||
|
kFrontendMaxAtomicCounterBufferSize,
|
||||||
|
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
||||||
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
break;
|
break;
|
||||||
@@ -2034,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;
|
||||||
@@ -2090,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)));
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetIntegerv(GLenum pname, GLint* params);
|
void GetIntegerv(GLenum pname, GLint* params);
|
||||||
void GetInteger64v(GLenum pname, GLint64* params);
|
void GetInteger64v(GLenum pname, GLint64* params);
|
||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
|
void GetFloati_v(GLenum target, GLuint index, GLfloat* data);
|
||||||
|
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
GLenum GetError();
|
GLenum GetError();
|
||||||
GLenum GetGraphicsResetStatus();
|
GLenum GetGraphicsResetStatus();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLboolean IsProgram(GLuint program);
|
GLboolean IsProgram(GLuint program);
|
||||||
GLboolean IsShader(GLuint shader);
|
GLboolean IsShader(GLuint shader);
|
||||||
void LinkProgram(GLuint program);
|
void LinkProgram(GLuint program);
|
||||||
|
// GL_KHR_parallel_shader_compile / GL_ARB_parallel_shader_compile. Both names are the
|
||||||
|
// same entry point; see MaxShaderCompilerThreadsKHR_State for the semantics of count.
|
||||||
|
void MaxShaderCompilerThreadsKHR(GLuint count);
|
||||||
|
void MaxShaderCompilerThreadsARB(GLuint count);
|
||||||
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
|
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
|
||||||
void UseProgram(GLuint program);
|
void UseProgram(GLuint program);
|
||||||
void Uniform1f(GLint location, GLfloat v0);
|
void Uniform1f(GLint location, GLfloat v0);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,918 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "ProgramInterface.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||||
|
namespace {
|
||||||
|
// glslang folds atomic counters into synthesized blocks named
|
||||||
|
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||||
|
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
||||||
|
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
||||||
|
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
||||||
|
|
||||||
|
enum class BlockKind {
|
||||||
|
Uniform, // a real GL uniform block
|
||||||
|
GlobalUbo, // the synthesized MGL_GLOBAL_UBO: GL sees its members as default-block
|
||||||
|
AtomicCounter, // gl_AtomicCounterBlock_<binding>
|
||||||
|
Storage, // a shader storage block
|
||||||
|
};
|
||||||
|
|
||||||
|
// One row of any interface. Fields a given interface does not have keep the
|
||||||
|
// spec-mandated "not applicable" value, so a prop read never has to special-case
|
||||||
|
// the interface a second time.
|
||||||
|
struct Resource {
|
||||||
|
String name;
|
||||||
|
GLenum type = GL_NONE;
|
||||||
|
GLint arraySize = 1;
|
||||||
|
GLint location = -1;
|
||||||
|
GLint locationIndex = -1;
|
||||||
|
GLint blockIndex = -1;
|
||||||
|
GLint offset = -1;
|
||||||
|
GLint arrayStride = -1;
|
||||||
|
GLint matrixStride = -1;
|
||||||
|
GLint isRowMajor = 0;
|
||||||
|
GLint atomicCounterBufferIndex = -1;
|
||||||
|
GLint topLevelArraySize = 0;
|
||||||
|
GLint topLevelArrayStride = 0;
|
||||||
|
GLint bufferBinding = 0;
|
||||||
|
GLint bufferDataSize = 0;
|
||||||
|
GLint isPerPatch = 0;
|
||||||
|
GLint xfbBufferIndex = 0;
|
||||||
|
Uint32 stages = 0; // EShLanguageMask
|
||||||
|
Vector<GLuint> activeVariables;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ResourceList = Vector<Resource>;
|
||||||
|
|
||||||
|
struct Model {
|
||||||
|
ResourceList uniforms;
|
||||||
|
ResourceList uniformBlocks;
|
||||||
|
ResourceList atomicCounterBuffers;
|
||||||
|
ResourceList bufferVariables;
|
||||||
|
ResourceList storageBlocks;
|
||||||
|
ResourceList programInputs;
|
||||||
|
ResourceList programOutputs;
|
||||||
|
ResourceList xfbVaryings;
|
||||||
|
Bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResourceList& EmptyList() {
|
||||||
|
static const ResourceList empty;
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- name spelling (cluster 6) -------------------------------------------------
|
||||||
|
|
||||||
|
Bool EndsWithZeroSubscript(const String& name) {
|
||||||
|
return name.length() >= 3 && name.compare(name.length() - 3, 3, "[0]") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||||
|
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||||
|
// stage inputs/outputs, so those get it here.
|
||||||
|
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
||||||
|
return name + "[0]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
||||||
|
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
||||||
|
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
||||||
|
if (type != nullptr && type->isArray()) {
|
||||||
|
if (!type->isSizedArray()) return 0;
|
||||||
|
return type->getOuterArraySize();
|
||||||
|
}
|
||||||
|
return reflectedSize < 1 ? 1 : reflectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two spellings name the same resource when they are equal, or differ only by the
|
||||||
|
// "[0]" the enumeration appends to an array.
|
||||||
|
Bool NamesMatch(const String& resourceName, const String& query) {
|
||||||
|
if (resourceName == query) return true;
|
||||||
|
if (EndsWithZeroSubscript(resourceName) &&
|
||||||
|
resourceName.compare(0, resourceName.length() - 3, query) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return EndsWithZeroSubscript(query) && query.compare(0, query.length() - 3, resourceName) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits "base[k]" into ("base", k). GL 4.6 §7.3.1.1 requires the subscript to be a
|
||||||
|
// decimal integer with no white space and no leading zeros, which is exactly what
|
||||||
|
// separates array-names' "a[1]" (resolves) from "a[01]", "a[0 + 0]" and "a[ 0]" (do
|
||||||
|
// not). Returns false when there is no trailing subscript at all; sets `malformed`
|
||||||
|
// when there is one but it is not a strict decimal.
|
||||||
|
Bool SplitTrailingSubscript(const String& name, String& outBase, Uint& outElement, Bool& outMalformed) {
|
||||||
|
outMalformed = false;
|
||||||
|
if (name.empty() || name.back() != ']') return false;
|
||||||
|
const SizeT bracket = name.rfind('[');
|
||||||
|
if (bracket == String::npos) return false;
|
||||||
|
const SizeT first = bracket + 1;
|
||||||
|
const SizeT last = name.length() - 1; // one past the digits
|
||||||
|
if (first >= last) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No leading zeros: "0" is the only spelling that may start with '0'.
|
||||||
|
if (name[first] == '0' && last - first > 1) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uint element = 0;
|
||||||
|
for (SizeT i = first; i < last; ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||||
|
if (element > 0x0FFFFFFFu) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outBase = name.substr(0, bracket);
|
||||||
|
outElement = element;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- block classification ------------------------------------------------------
|
||||||
|
|
||||||
|
Bool IsAtomicCounterBlockName(const String& name) {
|
||||||
|
return name.compare(0, std::strlen(kAtomicCounterBlockPrefix), kAtomicCounterBlockPrefix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "gl_AtomicCounterBlock_5" -> 5. The suffix is the GL binding the counters were
|
||||||
|
// declared with, which glslang does NOT keep in the block's own layout qualifier
|
||||||
|
// (that one is remapped to a plain buffer binding).
|
||||||
|
GLint AtomicCounterBlockBinding(const String& name) {
|
||||||
|
const SizeT underscore = name.rfind('_');
|
||||||
|
if (underscore == String::npos || underscore + 1 >= name.length()) return 0;
|
||||||
|
GLint binding = 0;
|
||||||
|
for (SizeT i = underscore + 1; i < name.length(); ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') return 0;
|
||||||
|
binding = binding * 10 + (name[i] - '0');
|
||||||
|
}
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element index of an arrayed block instance ("TrickyBuffer[1]" -> 1).
|
||||||
|
GLint BlockArrayElement(const String& name) {
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
if (!SplitTrailingSubscript(name, base, element, malformed)) return 0;
|
||||||
|
return static_cast<GLint>(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
||||||
|
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||||
|
return BlockKind::GlobalUbo;
|
||||||
|
}
|
||||||
|
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||||
|
const glslang::TType* type = block.getType();
|
||||||
|
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
||||||
|
return BlockKind::Uniform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||||
|
// uniform matrices. 0 for a non-matrix.
|
||||||
|
GLint MatrixStrideOf(const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
|
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
||||||
|
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
||||||
|
constexpr int scalarSize = 4;
|
||||||
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
|
: 4 * scalarSize;
|
||||||
|
return (vectorAlignment + 15) & ~15;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint IsRowMajorOf(const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
|
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint MappedLocation(Int rawLocation) {
|
||||||
|
// glslang parks "no location" at layoutLocationEnd; GL spells it -1.
|
||||||
|
if (rawLocation < 0 || rawLocation >= static_cast<Int>(glslang::TQualifier::layoutLocationEnd)) return -1;
|
||||||
|
return rawLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- model construction --------------------------------------------------------
|
||||||
|
|
||||||
|
// GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
|
||||||
|
//
|
||||||
|
// glslang records a block reference by walking up to the base symbol and calling
|
||||||
|
// addBlockName with the whole ARRAY type, which ORs the referencing stage into every
|
||||||
|
// element at once - it has not resolved the subscript yet at that point. So reading
|
||||||
|
// "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
|
||||||
|
// stage (KHR-GL43.program_interface_query.uniform-block-types).
|
||||||
|
//
|
||||||
|
// The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
|
||||||
|
// every element with the stage mask suppressed, and only the dereference chain actually
|
||||||
|
// walked turns a bit on - and that chain carries the subscript. So the union of a block
|
||||||
|
// instance's members is the reference set of that instance.
|
||||||
|
//
|
||||||
|
// Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
|
||||||
|
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
||||||
|
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
||||||
|
// back to the block's own mask there would restore the over-approximation.
|
||||||
|
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
||||||
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
|
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||||
|
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
||||||
|
for (Int index = 0; index < uniformCount; ++index) {
|
||||||
|
const auto& uniform = mutableReflection.getUniform(index);
|
||||||
|
const Int owner = uniform.index;
|
||||||
|
if (owner < 0 || owner >= blockCount) continue;
|
||||||
|
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||||
|
}
|
||||||
|
return stagesByBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
|
||||||
|
// names glslang produces for a uniform block array carry the subscript
|
||||||
|
// ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
|
||||||
|
// distinct entries and the bits land on the right one. A SHADER STORAGE block array does
|
||||||
|
// NOT get that treatment - its buffer variables reflect under one subscript-free spelling
|
||||||
|
// shared by every element - so a union over them credits element 0 and starves the rest.
|
||||||
|
// KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
|
||||||
|
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
||||||
|
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
||||||
|
// blocks therefore keep that mask untouched.
|
||||||
|
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||||
|
Int tIndex) {
|
||||||
|
String arrayBase;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
|
||||||
|
return static_cast<Uint32>(block.stages);
|
||||||
|
}
|
||||||
|
if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
|
||||||
|
return static_cast<Uint32>(block.stages);
|
||||||
|
}
|
||||||
|
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
|
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||||
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
|
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||||
|
|
||||||
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
|
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||||
|
const BlockKind kind = ClassifyBlock(block);
|
||||||
|
blockKind[tIndex] = kind;
|
||||||
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
|
Resource resource;
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER resources have no name (and GetProgramResource
|
||||||
|
// Index/Name reject the interface outright, which is why this stays empty).
|
||||||
|
resource.bufferBinding = AtomicCounterBlockBinding(block.name);
|
||||||
|
resource.bufferDataSize = block.size;
|
||||||
|
resource.stages = static_cast<Uint32>(block.stages);
|
||||||
|
blockInterfaceIndex[tIndex] = static_cast<Int>(model.atomicCounterBuffers.size());
|
||||||
|
model.atomicCounterBuffers.push_back(Move(resource));
|
||||||
|
} else if (kind == BlockKind::Storage) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = block.name;
|
||||||
|
// glslang reports the DECLARED binding for every instance of an arrayed
|
||||||
|
// block; GL gives element k the binding base + k. That is only the initial
|
||||||
|
// value: GL_BUFFER_BINDING must report the CURRENT binding, so a later
|
||||||
|
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||||
|
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||||
|
// GetUniformBlockBinding below).
|
||||||
|
const GLint declared = block.getBinding();
|
||||||
|
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
|
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||||
|
resource.bufferDataSize = block.size;
|
||||||
|
resource.stages = static_cast<Uint32>(block.stages);
|
||||||
|
blockInterfaceIndex[tIndex] = static_cast<Int>(model.storageBlocks.size());
|
||||||
|
model.storageBlocks.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||||
|
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||||
|
// with them (which is exactly what the CTS does).
|
||||||
|
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
||||||
|
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = program.GetUniformBlockName(glIndex);
|
||||||
|
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
||||||
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||||
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||||
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
|
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
||||||
|
stagesFromMembers, tIndex);
|
||||||
|
}
|
||||||
|
model.uniformBlocks.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
|
const Vector<BlockKind>& blockKind,
|
||||||
|
const Vector<Int>& blockInterfaceIndex) {
|
||||||
|
const Uint uniformCount = program.GetUniformCount();
|
||||||
|
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||||
|
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||||
|
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
const Int owner = refl.index;
|
||||||
|
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||||
|
? blockKind[owner]
|
||||||
|
: BlockKind::GlobalUbo;
|
||||||
|
|
||||||
|
Resource resource;
|
||||||
|
resource.name = refl.name;
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
|
||||||
|
if (kind == BlockKind::Storage) {
|
||||||
|
resource.blockIndex = blockInterfaceIndex[owner];
|
||||||
|
resource.offset = refl.offset;
|
||||||
|
resource.arrayStride = refl.arrayStride;
|
||||||
|
resource.matrixStride = MatrixStrideOf(type);
|
||||||
|
resource.isRowMajor = IsRowMajorOf(type);
|
||||||
|
// GL requires 1 for a member that is not inside a top-level array (and for
|
||||||
|
// the top-level array itself); glslang leaves 0/-1 there.
|
||||||
|
resource.topLevelArraySize = refl.topLevelArraySize > 0 ? refl.topLevelArraySize : 1;
|
||||||
|
resource.topLevelArrayStride = refl.topLevelArrayStride;
|
||||||
|
model.bufferVariables.push_back(Move(resource));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
|
// An atomic counter is a default-block uniform with no location and no
|
||||||
|
// owning uniform block; what it does have is a buffer to point at.
|
||||||
|
resource.type = GL_UNSIGNED_INT_ATOMIC_COUNTER;
|
||||||
|
resource.blockIndex = -1;
|
||||||
|
resource.offset = refl.offset;
|
||||||
|
resource.arrayStride = refl.arrayStride;
|
||||||
|
resource.matrixStride = 0;
|
||||||
|
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||||
|
resource.location = -1;
|
||||||
|
} else {
|
||||||
|
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
||||||
|
resource.offset = program.GetActiveUniformOffset(glIndex);
|
||||||
|
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
||||||
|
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
||||||
|
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
||||||
|
// A member of a named uniform block has no location, whatever the
|
||||||
|
// frontend's own location table says (it hands one out to every uniform
|
||||||
|
// so glUniform* can address block members through the global UBO).
|
||||||
|
resource.location =
|
||||||
|
resource.blockIndex >= 0 ? -1 : program.GetUniformLocation(refl.name);
|
||||||
|
}
|
||||||
|
model.uniforms.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_ACTIVE_VARIABLES, both directions.
|
||||||
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
|
const Resource& uniform = model.uniforms[i];
|
||||||
|
if (uniform.atomicCounterBufferIndex >= 0 &&
|
||||||
|
uniform.atomicCounterBufferIndex < static_cast<GLint>(model.atomicCounterBuffers.size())) {
|
||||||
|
model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back(
|
||||||
|
static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
||||||
|
// Members of an arrayed block are reflected once, against instance [0].
|
||||||
|
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
||||||
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
|
if (model.uniforms[i].blockIndex == owner) {
|
||||||
|
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SizeT blockIndex = 0; blockIndex < model.storageBlocks.size(); ++blockIndex) {
|
||||||
|
for (SizeT i = 0; i < model.bufferVariables.size(); ++i) {
|
||||||
|
if (model.bufferVariables[i].blockIndex == static_cast<GLint>(blockIndex)) {
|
||||||
|
model.storageBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A built-in interface block that a shader redeclares with fewer members keeps the
|
||||||
|
// omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
|
||||||
|
// (basic type void) instead of erasing them, because the original shared declaration
|
||||||
|
// has to stay usable. Only the instance-named form erases. So a separable vertex
|
||||||
|
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||||
|
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||||
|
// are not part of its output interface.
|
||||||
|
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
||||||
|
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||||
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
|
|
||||||
|
const Int inputCount = mutableReflection.getNumPipeInputs();
|
||||||
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
|
const auto& refl = mutableReflection.getPipeInput(index);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
|
Resource resource;
|
||||||
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
|
// names; GL enumerates the GL spellings.
|
||||||
|
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||||
|
resource.name = WithArraySuffix(glName, type);
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.location = program.GetAttributeLocation(refl.name);
|
||||||
|
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
||||||
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
model.programInputs.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A color number, and therefore a color INDEX, exists only for a fragment stage's
|
||||||
|
// outputs. The output interface belongs to the program's last stage, so for a
|
||||||
|
// separable tessellation/geometry/vertex program these are varyings: asking the
|
||||||
|
// frag-data maps about them can still answer a location (a tess-control output
|
||||||
|
// carries its own layout(location=N)), and a location then manufactures a color
|
||||||
|
// index of 0 where GL requires -1
|
||||||
|
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||||
|
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
||||||
|
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||||
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
|
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
|
Resource resource;
|
||||||
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
|
if (resource.location < 0 || !lastStageIsFragment) {
|
||||||
|
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||||
|
// non-fragment stage's outputs have no color number at all - either way there
|
||||||
|
// is no color index.
|
||||||
|
resource.locationIndex = -1;
|
||||||
|
} else {
|
||||||
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
|
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||||
|
// layout(index = N), which the frag-data maps never saw.
|
||||||
|
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
||||||
|
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
model.programOutputs.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildXfb(ProgramObject& program, Model& model) {
|
||||||
|
const auto& requested = program.GetTransformFeedbackInterfaceNames();
|
||||||
|
const auto& captured = program.GetTransformFeedbackVaryings();
|
||||||
|
for (const String& name : requested) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = name;
|
||||||
|
// ARB_transform_feedback3's layout controls are enumerated as resources of
|
||||||
|
// type NONE: gl_NextBuffer with array size 0, gl_SkipComponentsN with N.
|
||||||
|
if (name == "gl_NextBuffer") {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = 0;
|
||||||
|
} else if (name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' &&
|
||||||
|
name[17] <= '4') {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = name[17] - '0';
|
||||||
|
} else {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = 1;
|
||||||
|
for (const auto& varying : captured) {
|
||||||
|
if (varying.name != name) continue;
|
||||||
|
resource.type = varying.type;
|
||||||
|
resource.arraySize = varying.size < 1 ? 1 : varying.size;
|
||||||
|
resource.offset = static_cast<GLint>(varying.offsetBytes);
|
||||||
|
resource.xfbBufferIndex = static_cast<GLint>(varying.bufferIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.xfbVaryings.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Model BuildModel(ProgramObject& program) {
|
||||||
|
Model model;
|
||||||
|
if (!program.GetLinkStatus()) return model;
|
||||||
|
const glslang::TProgram* reflection = program.GetReflection();
|
||||||
|
if (reflection == nullptr) return model;
|
||||||
|
model.valid = true;
|
||||||
|
|
||||||
|
Vector<BlockKind> blockKind;
|
||||||
|
Vector<Int> blockInterfaceIndex;
|
||||||
|
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
|
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
|
BuildStageIO(program, *reflection, model);
|
||||||
|
BuildXfb(program, model);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceList& Select(const Model& model, GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
return model.uniforms;
|
||||||
|
case GL_UNIFORM_BLOCK:
|
||||||
|
return model.uniformBlocks;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER:
|
||||||
|
return model.atomicCounterBuffers;
|
||||||
|
case GL_BUFFER_VARIABLE:
|
||||||
|
return model.bufferVariables;
|
||||||
|
case GL_SHADER_STORAGE_BLOCK:
|
||||||
|
return model.storageBlocks;
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
return model.programInputs;
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
return model.programOutputs;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||||
|
return model.xfbVaryings;
|
||||||
|
default:
|
||||||
|
// The subroutine interfaces are accepted by the API but nothing can populate
|
||||||
|
// them: glslang refuses `subroutine` when generating SPIR-V, so a program
|
||||||
|
// using one never links. Zero active resources is the honest answer.
|
||||||
|
return EmptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool IsInterfaceEnum(GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
case GL_UNIFORM_BLOCK:
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
case GL_BUFFER_VARIABLE:
|
||||||
|
case GL_SHADER_STORAGE_BLOCK:
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER:
|
||||||
|
case GL_VERTEX_SUBROUTINE:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE:
|
||||||
|
case GL_COMPUTE_SUBROUTINE:
|
||||||
|
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsNamedInterface(GLenum programInterface) {
|
||||||
|
// GL 4.6 §7.3.1.2: the two buffer interfaces have no resource names, and asking for
|
||||||
|
// one is INVALID_ENUM (deliberately asymmetric with GetProgramInterfaceiv, which
|
||||||
|
// does count them).
|
||||||
|
return IsInterfaceEnum(programInterface) && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
|
||||||
|
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool InterfaceHasLocations(GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsResourceProp(GLenum prop) {
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
case GL_TYPE:
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
case GL_OFFSET:
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
case GL_LOCATION:
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||||
|
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||||
|
case GL_COMPATIBLE_SUBROUTINES:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 Table 7.2, transcribed row by row: which interfaces each property applies to.
|
||||||
|
// Too tight a table turns a currently-answered prop into a fresh INVALID_OPERATION, so
|
||||||
|
// the rows below are deliberately no narrower than the spec's.
|
||||||
|
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop) {
|
||||||
|
const Bool isSubroutine =
|
||||||
|
programInterface == GL_VERTEX_SUBROUTINE || programInterface == GL_TESS_CONTROL_SUBROUTINE ||
|
||||||
|
programInterface == GL_TESS_EVALUATION_SUBROUTINE || programInterface == GL_GEOMETRY_SUBROUTINE ||
|
||||||
|
programInterface == GL_FRAGMENT_SUBROUTINE || programInterface == GL_COMPUTE_SUBROUTINE;
|
||||||
|
const Bool isSubroutineUniform =
|
||||||
|
programInterface == GL_VERTEX_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_TESS_CONTROL_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_TESS_EVALUATION_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_GEOMETRY_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_FRAGMENT_SUBROUTINE_UNIFORM || programInterface == GL_COMPUTE_SUBROUTINE_UNIFORM;
|
||||||
|
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
return programInterface != GL_ATOMIC_COUNTER_BUFFER && programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_TYPE:
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_PROGRAM_INPUT ||
|
||||||
|
programInterface == GL_PROGRAM_OUTPUT || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_VARYING ||
|
||||||
|
(prop == GL_ARRAY_SIZE && isSubroutineUniform);
|
||||||
|
case GL_OFFSET:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
return programInterface == GL_UNIFORM;
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
// Table 7.2 lists GL_TRANSFORM_FEEDBACK_BUFFER on these three rows too. This
|
||||||
|
// implementation enumerates no resources on that interface, so the query still
|
||||||
|
// ends in an error - but INVALID_VALUE for the out-of-range index, not the
|
||||||
|
// INVALID_OPERATION a narrower table would invent.
|
||||||
|
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK;
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_UNIFORM_BLOCK ||
|
||||||
|
programInterface == GL_ATOMIC_COUNTER_BUFFER || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK || programInterface == GL_PROGRAM_INPUT ||
|
||||||
|
programInterface == GL_PROGRAM_OUTPUT || isSubroutineUniform;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
return programInterface == GL_BUFFER_VARIABLE;
|
||||||
|
case GL_LOCATION:
|
||||||
|
return InterfaceHasLocations(programInterface);
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
return programInterface == GL_PROGRAM_OUTPUT;
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
return programInterface == GL_PROGRAM_INPUT || programInterface == GL_PROGRAM_OUTPUT;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
return programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||||
|
return programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||||
|
case GL_COMPATIBLE_SUBROUTINES:
|
||||||
|
return isSubroutineUniform;
|
||||||
|
default:
|
||||||
|
(void)isSubroutine;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
return static_cast<Int>(Select(model, programInterface).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface) {
|
||||||
|
if (!IsNamedInterface(programInterface)) return 0;
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
SizeT longest = 0;
|
||||||
|
for (const Resource& resource : Select(model, programInterface)) {
|
||||||
|
longest = std::max(longest, resource.name.length() + 1);
|
||||||
|
}
|
||||||
|
return static_cast<Int>(longest);
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
SizeT longest = 0;
|
||||||
|
for (const Resource& resource : Select(model, programInterface)) {
|
||||||
|
longest = std::max(longest, resource.activeVariables.size());
|
||||||
|
}
|
||||||
|
return static_cast<Int>(longest);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (name == nullptr || name[0] == '\0') return GL_INVALID_INDEX;
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
const String query = name;
|
||||||
|
// The layout controls of an interleaved capture are enumerable but not addressable
|
||||||
|
// by name (GL 4.6 §7.3.1.1).
|
||||||
|
if (programInterface == GL_TRANSFORM_FEEDBACK_VARYING &&
|
||||||
|
(query == "gl_NextBuffer" ||
|
||||||
|
(query.size() == 18 && query.compare(0, 17, "gl_SkipComponents") == 0))) {
|
||||||
|
return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < resources.size(); ++i) {
|
||||||
|
if (NamesMatch(resources[i].name, query)) return static_cast<GLuint>(i);
|
||||||
|
}
|
||||||
|
return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
if (index >= resources.size()) return false;
|
||||||
|
outName = resources[index].name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||||
|
Vector<GLint>& outValues) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
if (index >= resources.size()) return false;
|
||||||
|
const Resource& resource = resources[index];
|
||||||
|
|
||||||
|
const auto referencedBy = [&resource](EShLanguage stage) {
|
||||||
|
return (resource.stages & static_cast<Uint32>(1u << stage)) != 0 ? GL_TRUE : GL_FALSE;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.name.length() + 1));
|
||||||
|
break;
|
||||||
|
case GL_TYPE:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.type));
|
||||||
|
break;
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
outValues.push_back(resource.arraySize);
|
||||||
|
break;
|
||||||
|
case GL_OFFSET:
|
||||||
|
outValues.push_back(resource.offset);
|
||||||
|
break;
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
outValues.push_back(resource.blockIndex);
|
||||||
|
break;
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
outValues.push_back(resource.arrayStride);
|
||||||
|
break;
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
outValues.push_back(resource.matrixStride);
|
||||||
|
break;
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
outValues.push_back(resource.isRowMajor);
|
||||||
|
break;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
outValues.push_back(resource.atomicCounterBufferIndex);
|
||||||
|
break;
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
outValues.push_back(resource.bufferBinding);
|
||||||
|
break;
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
outValues.push_back(resource.bufferDataSize);
|
||||||
|
break;
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.activeVariables.size()));
|
||||||
|
break;
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast<GLint>(variable));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangVertex));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangTessControl));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangTessEvaluation));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangGeometry));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangFragment));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangCompute));
|
||||||
|
break;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
outValues.push_back(resource.topLevelArraySize);
|
||||||
|
break;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
outValues.push_back(resource.topLevelArrayStride);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION:
|
||||||
|
outValues.push_back(resource.location);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
outValues.push_back(resource.locationIndex);
|
||||||
|
break;
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
outValues.push_back(resource.isPerPatch);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
outValues.push_back(0);
|
||||||
|
break;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
outValues.push_back(resource.xfbBufferIndex);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
outValues.push_back(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (name == nullptr || name[0] == '\0') return -1;
|
||||||
|
const String query = name;
|
||||||
|
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||||
|
if (malformed) return -1;
|
||||||
|
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
for (const Resource& resource : resources) {
|
||||||
|
if (NamesMatch(resource.name, query)) return resource.location;
|
||||||
|
}
|
||||||
|
if (!subscripted || element == 0) return -1;
|
||||||
|
// "d[1]" addresses the second element of an array resource enumerated as "d[0]".
|
||||||
|
for (const Resource& resource : resources) {
|
||||||
|
if (!NamesMatch(resource.name, base)) continue;
|
||||||
|
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||||
|
return resource.location + static_cast<GLint>(element);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (programInterface != GL_PROGRAM_OUTPUT || name == nullptr || name[0] == '\0') return -1;
|
||||||
|
const String query = name;
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||||
|
if (malformed) return -1;
|
||||||
|
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
for (const Resource& resource : model.programOutputs) {
|
||||||
|
if (NamesMatch(resource.name, query)) return resource.locationIndex;
|
||||||
|
}
|
||||||
|
if (!subscripted) return -1;
|
||||||
|
for (const Resource& resource : model.programOutputs) {
|
||||||
|
if (!NamesMatch(resource.name, base)) continue;
|
||||||
|
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||||
|
return resource.locationIndex;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class ProgramObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL program interface (ARB_program_interface_query / GL 4.3 §7.3.1) as a frontend
|
||||||
|
// resource model.
|
||||||
|
//
|
||||||
|
// WHY IT IS HERE AND NOT IN A BACKEND. glGetProgramResource* describes the program the
|
||||||
|
// APPLICATION wrote, in the application's namespace. Neither backend program is in that
|
||||||
|
// namespace: DirectGLES compiles SPIRV-Cross-generated ESSL where default-block uniforms
|
||||||
|
// live inside the synthesized MGL_GLOBAL_UBO (so a GL_UNIFORM location query against it is
|
||||||
|
// structurally -1) and stage in/out names are rewritten; DirectVulkan has no GL-level
|
||||||
|
// reflection at all and can only re-derive a partial, diverging copy. The one authoritative
|
||||||
|
// source is the frontend glslang reflection a link already produced, which is the same
|
||||||
|
// place glGetActiveUniform answers from. This layer generalizes that rule to every
|
||||||
|
// interface, so the six entry points never consult gBackendFunctionsTable.
|
||||||
|
//
|
||||||
|
// NAMING RULES LIVE HERE, NOT IN ProgramObject. The interface query spells resources
|
||||||
|
// differently from glGetActiveUniform / glGetActiveAttrib (an array is "name[0]", a lookup
|
||||||
|
// accepts both "name" and "name[0]", a subscript must be a strict decimal). Those two
|
||||||
|
// getters are what GL30-33 exercises and they must not move, so every normalization is
|
||||||
|
// applied on the way in and out of THIS file.
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||||
|
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||||
|
|
||||||
|
// <programInterface> is one of the GL 4.6 Table 7.1 interfaces.
|
||||||
|
Bool IsInterfaceEnum(GLenum programInterface);
|
||||||
|
// Interfaces whose resources have names (everything except GL_ATOMIC_COUNTER_BUFFER).
|
||||||
|
Bool IsNamedInterface(GLenum programInterface);
|
||||||
|
// <prop> is a property token GetProgramResourceiv knows at all (else GL_INVALID_ENUM).
|
||||||
|
Bool IsResourceProp(GLenum prop);
|
||||||
|
// <prop> applies to <programInterface> (else GL_INVALID_OPERATION).
|
||||||
|
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop);
|
||||||
|
// Interfaces GetProgramResourceLocation accepts (else GL_INVALID_ENUM).
|
||||||
|
Bool InterfaceHasLocations(GLenum programInterface);
|
||||||
|
|
||||||
|
// GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH / GL_MAX_NUM_ACTIVE_VARIABLES. All three
|
||||||
|
// report zero for an interface this implementation cannot enumerate and for a program
|
||||||
|
// that has not linked successfully - which is what the spec requires of a program with
|
||||||
|
// no active resources.
|
||||||
|
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface);
|
||||||
|
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface);
|
||||||
|
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface);
|
||||||
|
|
||||||
|
// GL_INVALID_INDEX when <name> names no active resource of the interface.
|
||||||
|
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
// False when <index> is out of range for the interface (the caller raises INVALID_VALUE).
|
||||||
|
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName);
|
||||||
|
// Appends the value(s) of <prop> for the resource; GL_ACTIVE_VARIABLES appends several.
|
||||||
|
// False when <index> is out of range.
|
||||||
|
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||||
|
Vector<GLint>& outValues);
|
||||||
|
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||||
@@ -648,4 +648,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
GetQueryiv(target, pname, params);
|
GetQueryiv(target, pname, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DestroyAllQueryObjects() {
|
||||||
|
// Detach the registry under the lock, release outside it - same discipline
|
||||||
|
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
|
||||||
|
// this drain, every query the app left undeleted survived full library
|
||||||
|
// teardown in the process-global registry: the objects and their backend
|
||||||
|
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
|
||||||
|
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
|
||||||
|
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
|
||||||
|
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
|
||||||
|
UnorderedMap<GLuint, QueryObject*> orphans;
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
orphans.swap(g_liveQueryObjects);
|
||||||
|
g_activeTimeElapsedQueryId = 0;
|
||||||
|
g_activePrimitivesWrittenQueryId = 0;
|
||||||
|
g_activePrimitivesGeneratedQueryId = 0;
|
||||||
|
g_activeSamplesPassedQueryId = 0;
|
||||||
|
}
|
||||||
|
if (orphans.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Backend handles must be released by the backend that created them, so
|
||||||
|
// this runs while the function table is still populated. Both backends'
|
||||||
|
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
|
||||||
|
// or ES context is already gone frees only the wrapper.
|
||||||
|
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||||
|
for (const auto& [_, queryObject] : orphans) {
|
||||||
|
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
delete queryObject;
|
||||||
|
}
|
||||||
|
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -29,4 +29,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void QueryCounter(GLuint id, GLenum target);
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
|
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||||
|
// GL requires queries to die with their context; called only from full library
|
||||||
|
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||||
|
// process-global registry can be drained wholesale. Must run while the backend
|
||||||
|
// function table is still populated: each backend handle has to be released by
|
||||||
|
// the backend that created it, never by a later re-initialized one (whose
|
||||||
|
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
|
||||||
|
// Same contract as DestroyAllSyncObjects.
|
||||||
|
void DestroyAllQueryObjects();
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "GL_Sampler.h"
|
#include "GL_Sampler.h"
|
||||||
#include "Validators.h"
|
#include "Validators.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>
|
||||||
@@ -268,9 +270,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The number of texture units a sampler may be bound to is the same count a TEXTURE may be
|
||||||
|
// bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
|
||||||
|
// computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
|
||||||
|
// readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
|
||||||
|
// come to disagree about which units exist.
|
||||||
|
static GLint GetSamplerBindableTextureUnitCount() {
|
||||||
|
return GetCombinedTextureImageUnitCount();
|
||||||
|
}
|
||||||
|
|
||||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||||
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
|
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
|
||||||
if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
if (static_cast<Uint64>(unit) >= static_cast<Uint64>(GetSamplerBindableTextureUnitCount())) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
|
||||||
@@ -309,9 +320,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ARB_multi_bind: the whole [first, first + count) range is checked up front and a
|
||||||
|
// range that runs past the last texture unit is INVALID_OPERATION - not the
|
||||||
|
// INVALID_VALUE the single-bind BindSampler_State reports per element, and nothing is
|
||||||
|
// bound when it fails. Both gates read the same limit (see
|
||||||
|
// GetSamplerBindableTextureUnitCount), so an out-of-range multi-bind can no longer slip
|
||||||
|
// past this check and be caught one element at a time with the wrong error class.
|
||||||
|
const GLint maxTextureUnits = GetSamplerBindableTextureUnitCount();
|
||||||
|
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(maxTextureUnits)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers",
|
||||||
|
"first + count exceeds the number of texture units."));
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case GL_TEXTURE_COMPARE_FUNC:
|
case GL_TEXTURE_COMPARE_FUNC:
|
||||||
if (param < GL_LEQUAL || param > GL_ALWAYS) {
|
// The eight depth-compare functions are contiguous from GL_NEVER (0x0200) to
|
||||||
|
// GL_ALWAYS (0x0207); GL_LEQUAL sits in the middle of that block, so starting
|
||||||
|
// the range there rejected NEVER/LESS/EQUAL and let GREATER/NOTEQUAL/GEQUAL
|
||||||
|
// through only by accident of them being above LEQUAL.
|
||||||
|
if (param < GL_NEVER || param > GL_ALWAYS) {
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerParam",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerParam",
|
||||||
"Invalid compare function parameter"));
|
"Invalid compare function parameter"));
|
||||||
|
|||||||
@@ -86,17 +86,65 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DSA emulation: the by-name entry points are implemented by putting the named texture
|
||||||
|
// on the active unit's slot for their target, running the classic bound-texture code,
|
||||||
|
// then putting the previous binding back.
|
||||||
|
//
|
||||||
|
// Both of those binds are REAL changes to "which texture is bound at this unit" for as
|
||||||
|
// long as `fn` runs, so both have to move the texture bind generation. Backends memoise
|
||||||
|
// per-unit work keyed on that generation and BORROW the binding slot (they hold a
|
||||||
|
// pointer to the slot's shared_ptr, not a copy); a slot swap the generation never saw
|
||||||
|
// let such a memo replay texture A's backend twin against texture B now sitting in the
|
||||||
|
// slot - which re-specified A's backend storage with B's shape and silently destroyed
|
||||||
|
// A's GPU-rendered contents (Minecraft's lightmap, blanked by a by-name upload to an
|
||||||
|
// Iris shadow map, which then discarded every glyph).
|
||||||
|
//
|
||||||
|
// The generation is bumped directly rather than through NoteTextureUnitTouched because
|
||||||
|
// the touched-unit HIGH-WATER MARK must NOT move: glActiveTexture does not advance it,
|
||||||
|
// so a DSA-only app would otherwise have every later draw walk up to the highest unit it
|
||||||
|
// ever aimed a by-name call at. Not advancing it is also sufficient - a unit above the
|
||||||
|
// mark is outside every memo's coverage and outside the epoch walk, so nothing can
|
||||||
|
// observe the transient swap there; at or below it, the bump is exactly what makes the
|
||||||
|
// epoch re-derive. Bumping only on a real change keeps the very common redundant case (a
|
||||||
|
// by-name call on the texture already bound to the active unit) free.
|
||||||
|
//
|
||||||
|
// The restore is a scope guard because `fn` can throw (the unsupported-state paths use
|
||||||
|
// THROW_EXCEPTION): leaking the temporary binding would leave the wrong texture bound to
|
||||||
|
// a live unit for the rest of the context's life.
|
||||||
template <typename Fn>
|
template <typename Fn>
|
||||||
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
Fn&& fn) {
|
Fn&& fn) {
|
||||||
if (!textureObject) return;
|
if (!textureObject) return;
|
||||||
|
|
||||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
const Int activeUnitIndex = MG_State::pGLContext->GetActiveTextureUnit();
|
||||||
|
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnitIndex);
|
||||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureObject->GetTarget());
|
auto& bindingSlot = activeUnit.GetBindingSlot(textureObject->GetTarget());
|
||||||
const auto previousBinding = bindingSlot.GetBoundObject();
|
const auto previousBinding = bindingSlot.GetBoundObject();
|
||||||
bindingSlot.Bind(textureObject);
|
|
||||||
|
using SlotType = std::remove_reference_t<decltype(bindingSlot)>;
|
||||||
|
class ScopedSlotRestore {
|
||||||
|
public:
|
||||||
|
ScopedSlotRestore(SlotType& slot, SharedPtr<MG_State::GLState::ITextureObject> previous)
|
||||||
|
: m_slot(slot), m_previous(Move(previous)) {}
|
||||||
|
~ScopedSlotRestore() {
|
||||||
|
if (m_slot.Bind(m_previous)) {
|
||||||
|
MG_State::pGLContext->BumpTextureBindGeneration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScopedSlotRestore(const ScopedSlotRestore&) = delete;
|
||||||
|
ScopedSlotRestore& operator=(const ScopedSlotRestore&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SlotType& m_slot;
|
||||||
|
SharedPtr<MG_State::GLState::ITextureObject> m_previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bindingSlot.Bind(textureObject)) {
|
||||||
|
MG_State::pGLContext->BumpTextureBindGeneration();
|
||||||
|
}
|
||||||
|
ScopedSlotRestore restore(bindingSlot, previousBinding);
|
||||||
|
|
||||||
fn(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
|
fn(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
|
||||||
bindingSlot.Bind(previousBinding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SizeT ComputeTextureStorageByteSize(TextureInternalFormat textureInternalFormat, GLsizei width, GLsizei height,
|
SizeT ComputeTextureStorageByteSize(TextureInternalFormat textureInternalFormat, GLsizei width, GLsizei height,
|
||||||
@@ -565,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) {
|
||||||
@@ -805,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;
|
||||||
}
|
}
|
||||||
@@ -1311,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);
|
||||||
@@ -1384,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;
|
||||||
}
|
}
|
||||||
@@ -1493,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;
|
||||||
@@ -1507,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;
|
||||||
}
|
}
|
||||||
@@ -2058,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);
|
||||||
}
|
}
|
||||||
@@ -2173,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) {
|
||||||
@@ -2187,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);
|
||||||
}
|
}
|
||||||
@@ -2862,7 +2985,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2876,7 +3000,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2890,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
|
||||||
@@ -2997,7 +3123,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3011,7 +3138,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3025,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
|
||||||
@@ -3253,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);
|
||||||
@@ -3267,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,
|
||||||
@@ -3285,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;
|
||||||
@@ -3355,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;
|
||||||
@@ -3378,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,
|
||||||
@@ -3389,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,
|
||||||
@@ -3485,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);
|
||||||
@@ -4016,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);
|
||||||
@@ -4024,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));
|
||||||
@@ -4031,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;
|
||||||
@@ -4357,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__);
|
||||||
@@ -4434,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;
|
||||||
@@ -5128,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
|
||||||
|
|||||||
@@ -106,6 +106,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two ARB_vertex_attrib_binding per-attribute queries. They do not live on the
|
||||||
|
// resolved VertexAttribute (which is the flat, already-combined view) but on the VAO's
|
||||||
|
// binding-point mapping, so they need the object, not the attribute.
|
||||||
|
static bool TryGetVertexAttribBindingQuery(GLuint index, GLenum pname, GLint& out) {
|
||||||
|
if (pname != GL_VERTEX_ATTRIB_BINDING && pname != GL_VERTEX_ATTRIB_RELATIVE_OFFSET) return false;
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (!vao) {
|
||||||
|
out = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
out = pname == GL_VERTEX_ATTRIB_BINDING ? static_cast<GLint>(vao->GetAttributeBindingIndex(index))
|
||||||
|
: static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
|
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
|
||||||
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
|
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
|
||||||
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
||||||
@@ -164,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:
|
||||||
@@ -179,6 +216,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
||||||
|
// ARB_vertex_attrib_binding (core since GL 4.3). The binding-point view is real
|
||||||
|
// state on the VAO (GetAttributeBindingIndex / GetAttributeRelativeOffset), so
|
||||||
|
// both of its per-attribute queries are answerable.
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -273,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,
|
||||||
@@ -303,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) {
|
||||||
@@ -482,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);
|
||||||
@@ -924,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));
|
||||||
@@ -944,6 +989,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLfloat>(attr->Divisor);
|
params[0] = static_cast<GLfloat>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||||
|
GLint value = 0;
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||||
|
params[0] = static_cast<GLfloat>(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -987,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));
|
||||||
@@ -1007,6 +1059,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLdouble>(attr->Divisor);
|
params[0] = static_cast<GLdouble>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||||
|
GLint value = 0;
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||||
|
params[0] = static_cast<GLdouble>(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -1045,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));
|
||||||
@@ -1066,6 +1128,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLint>(attr->Divisor);
|
params[0] = static_cast<GLint>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, params[0]);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -1100,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) {
|
||||||
@@ -1184,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));
|
||||||
@@ -1204,6 +1270,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
*param = static_cast<GLint>(vao->GetAttributeBindingIndex(index));
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -1253,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) {
|
||||||
@@ -1274,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);
|
||||||
}
|
}
|
||||||
@@ -1300,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;
|
||||||
@@ -1308,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);
|
||||||
|
|||||||
@@ -147,6 +147,30 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The integer path takes exactly the six signed/unsigned integer types (GL 4.6
|
||||||
|
// core 10.3.2): BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, INT, UNSIGNED_INT.
|
||||||
|
// A blacklist could not express that: GL_FLOAT, GL_HALF_FLOAT,
|
||||||
|
// GL_DOUBLE and GL_FIXED all convert to a perfectly valid DataType, so they slipped
|
||||||
|
// through and were recorded as integer attributes.
|
||||||
|
if (integerPath) {
|
||||||
|
switch (type) {
|
||||||
|
case DataType::Int8:
|
||||||
|
case DataType::Uint8:
|
||||||
|
case DataType::Int16:
|
||||||
|
case DataType::Uint16:
|
||||||
|
case DataType::Int32:
|
||||||
|
case DataType::Uint32:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("Type is not an integer vertex attribute type (attribute {}).", index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sizeRaw == static_cast<GLint>(GL_BGRA)) {
|
if (sizeRaw == static_cast<GLint>(GL_BGRA)) {
|
||||||
// GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and
|
// GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and
|
||||||
// normalized == GL_TRUE. On the integer path it is simply an out-of-range size.
|
// normalized == GL_TRUE. On the integer path it is simply an out-of-range size.
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
# MobileGL headless GPU integration tests.
|
||||||
|
#
|
||||||
|
# These are not unit tests: each scenario brings up a real EGL context on a
|
||||||
|
# pbuffer, renders real frames through a real backend and asserts on
|
||||||
|
# glReadPixels output. They need a GPU, so the module is OFF by default
|
||||||
|
# (MOBILEGL_BUILD_INTEGRATION_TEST) and every scenario skips cleanly - never
|
||||||
|
# fails, never hangs - on a machine without one. "Cleanly" is not a hope: the
|
||||||
|
# harness runs the whole bring-up in a forked child first, because MobileGL
|
||||||
|
# ABORTS rather than returning an error on an unusable platform (HeadlessGL.cpp).
|
||||||
|
#
|
||||||
|
# A clean skip is also indistinguishable from a pass, so set
|
||||||
|
# MOBILEGL_ITEST_REQUIRE_GPU wherever the machine is supposed to have a GPU.
|
||||||
|
#
|
||||||
|
# Backend selection is latched at initialization from MOBILEGL_BACKEND_TYPE, so
|
||||||
|
# one process is one backend: the same binary is registered twice, once per
|
||||||
|
# backend, under the `integration-gpu` label.
|
||||||
|
|
||||||
|
message(STATUS "Generating build files for MobileGL Integration Test...")
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||||
|
|
||||||
|
# Desktop links the static implementation directly. Android runs the same
|
||||||
|
# executable from adb shell and links the shipping shared library instead.
|
||||||
|
if (ANDROID)
|
||||||
|
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
|
||||||
|
elseif (TARGET MobileGL_s)
|
||||||
|
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
|
||||||
|
else()
|
||||||
|
message(STATUS "No MobileGL library target is available; skipping the integration test module")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# MG_Test already pulls googletest in when MOBILEGL_BUILD_TEST is ON. Stand on
|
||||||
|
# our own feet when it is not, so this module can be built by itself.
|
||||||
|
if (NOT TARGET GTest::gtest)
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
googletest
|
||||||
|
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||||
|
GIT_TAG v1.17.0
|
||||||
|
)
|
||||||
|
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(googletest)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(MobileGLIntegrationTest
|
||||||
|
Main.cpp
|
||||||
|
Harness/HeadlessGL.cpp
|
||||||
|
Scenarios/OrientationScenario.cpp
|
||||||
|
Scenarios/CrossFrameBufferScenario.cpp
|
||||||
|
Scenarios/ResidentIndexScenario.cpp
|
||||||
|
Scenarios/MultiDrawScenario.cpp
|
||||||
|
Scenarios/DrawParametersScenario.cpp
|
||||||
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
|
Scenarios/PipelineFailureScenario.cpp
|
||||||
|
Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
|
Scenarios/FragCoordOriginScenario.cpp
|
||||||
|
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||||
|
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||||
|
Scenarios/ClipDistanceScenario.cpp
|
||||||
|
Scenarios/ViewportArrayScenario.cpp
|
||||||
|
Scenarios/SsboArrayLengthScenario.cpp
|
||||||
|
Scenarios/DoublePrecisionScenario.cpp
|
||||||
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
|
Scenarios/IterationRPFirstReductionScenario.cpp
|
||||||
|
Scenarios/IterationRPProgram203Scenario.cpp
|
||||||
|
Scenarios/IterationRPScratchFixScenario.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
|
||||||
|
${MGL_ITEST_ROOT}/include
|
||||||
|
${MGL_ITEST_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||||
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
|
GTest::gtest
|
||||||
|
${MGL_ITEST_MOBILEGL_TARGET}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
|
||||||
|
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
|
||||||
|
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
|
||||||
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
|
${MGL_ITEST_ANDROID_LIBRARY}
|
||||||
|
${MGL_ITEST_LOG_LIBRARY}
|
||||||
|
${MGL_ITEST_MEDIANDK_LIBRARY}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
||||||
|
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
||||||
|
# resolve if the whole static library is part of the link.
|
||||||
|
target_link_options(MobileGLIntegrationTest PRIVATE /WHOLEARCHIVE:MobileGL_s)
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
||||||
|
|
||||||
|
if (ANDROID)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- ctest wiring --------------------------------------------------------
|
||||||
|
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
||||||
|
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
||||||
|
# a GPU test. Pin the vendor/ICD json the same way MG_Benchmark's
|
||||||
|
# run_driver_bench.sh does.
|
||||||
|
#
|
||||||
|
# Leaving these empty is not a neutral default, it is the failure mode: an
|
||||||
|
# unpinned libEGL lands on llvmpipe and the suite goes green having tested a
|
||||||
|
# software rasteriser. So they are DETECTED here rather than defaulted to empty,
|
||||||
|
# and an empty result is a loud warning.
|
||||||
|
#
|
||||||
|
# mgl_itest_find_driver_json(<outVar> <description> <glob> [<glob>...])
|
||||||
|
# Picks the first json a real hardware vendor owns, in preference order, and
|
||||||
|
# never picks a software rasteriser (llvmpipe / lavapipe / swrast) - landing on
|
||||||
|
# one of those silently is the exact accident this pinning exists to prevent.
|
||||||
|
function(mgl_itest_find_driver_json outVar)
|
||||||
|
set(candidates "")
|
||||||
|
foreach(pattern IN LISTS ARGN)
|
||||||
|
file(GLOB matches "${pattern}")
|
||||||
|
list(APPEND candidates ${matches})
|
||||||
|
endforeach()
|
||||||
|
list(SORT candidates)
|
||||||
|
# Vendors ship an i686 json beside the x86_64 one and it sorts first. Pinning
|
||||||
|
# the wrong word size is worse than not pinning at all - the loader finds no
|
||||||
|
# driver and the whole suite skips - so drop the mismatched ones outright.
|
||||||
|
if (CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
|
list(FILTER candidates EXCLUDE REGEX "i686|i386")
|
||||||
|
else()
|
||||||
|
list(FILTER candidates EXCLUDE REGEX "x86_64|aarch64")
|
||||||
|
endif()
|
||||||
|
set(software "")
|
||||||
|
foreach(vendor IN ITEMS nvidia amdgpu amd radeon intel_hasvk intel broadcom freedreno panfrost)
|
||||||
|
foreach(candidate IN LISTS candidates)
|
||||||
|
get_filename_component(leaf "${candidate}" NAME)
|
||||||
|
string(TOLOWER "${leaf}" leaf)
|
||||||
|
if (leaf MATCHES "${vendor}")
|
||||||
|
set(${outVar} "${candidate}" PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
# Nothing recognised as hardware. Report the first non-software entry if there
|
||||||
|
# is one; otherwise report nothing, so the warning below fires.
|
||||||
|
foreach(candidate IN LISTS candidates)
|
||||||
|
get_filename_component(leaf "${candidate}" NAME)
|
||||||
|
string(TOLOWER "${leaf}" leaf)
|
||||||
|
if (NOT leaf MATCHES "lvp|llvmpipe|lavapipe|swrast|softpipe")
|
||||||
|
set(${outVar} "${candidate}" PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set(software "${candidate}")
|
||||||
|
endforeach()
|
||||||
|
set(${outVar} "" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(MGL_ITEST_DETECTED_EGL_VENDOR "")
|
||||||
|
set(MGL_ITEST_DETECTED_VK_ICD "")
|
||||||
|
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||||
|
mgl_itest_find_driver_json(MGL_ITEST_DETECTED_EGL_VENDOR
|
||||||
|
"/usr/share/glvnd/egl_vendor.d/*.json"
|
||||||
|
"/etc/glvnd/egl_vendor.d/*.json")
|
||||||
|
mgl_itest_find_driver_json(MGL_ITEST_DETECTED_VK_ICD
|
||||||
|
"/usr/share/vulkan/icd.d/*.json"
|
||||||
|
"/etc/vulkan/icd.d/*.json")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MOBILEGL_ITEST_EGL_VENDOR "${MGL_ITEST_DETECTED_EGL_VENDOR}" CACHE FILEPATH
|
||||||
|
"glvnd EGL vendor json to pin for the integration tests (empty: leave the loader alone)")
|
||||||
|
set(MOBILEGL_ITEST_VK_ICD "${MGL_ITEST_DETECTED_VK_ICD}" CACHE FILEPATH
|
||||||
|
"Vulkan ICD json to pin for the DirectVulkan integration tests (empty: leave the loader alone)")
|
||||||
|
|
||||||
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
|
message(STATUS "Integration tests: pinning EGL vendor ${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
|
else()
|
||||||
|
message(WARNING
|
||||||
|
"Integration tests: no EGL vendor json found or configured (MOBILEGL_ITEST_EGL_VENDOR is empty). "
|
||||||
|
"An unpinned libEGL on a glvnd system resolves to whichever vendor comes first, which is usually "
|
||||||
|
"Mesa/llvmpipe - the scenarios would then go green against a software rasteriser instead of the GPU. "
|
||||||
|
"Set -DMOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/<vendor>.json.")
|
||||||
|
endif()
|
||||||
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
|
message(STATUS "Integration tests: pinning Vulkan ICD ${MOBILEGL_ITEST_VK_ICD}")
|
||||||
|
else()
|
||||||
|
message(WARNING
|
||||||
|
"Integration tests: no Vulkan ICD json found or configured (MOBILEGL_ITEST_VK_ICD is empty). "
|
||||||
|
"DirectVulkan would then load whichever ICD the loader enumerates first, quite possibly lavapipe. "
|
||||||
|
"Set -DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/<vendor>.json.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Turns "no usable GPU" from a clean skip into a failure - see ScenarioFixture.h.
|
||||||
|
# Without it the integration-gpu label is unfalsifiable: a run that skipped every
|
||||||
|
# scenario and a run that passed every scenario are the same green in ctest.
|
||||||
|
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||||
|
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
||||||
|
|
||||||
|
# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless
|
||||||
|
# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a
|
||||||
|
# developer's machine and a CI runner take the SAME path whether or not a window
|
||||||
|
# system happens to be running. This used to inject "x11", which is how the lane
|
||||||
|
# came up green on a workstation with WSLg and died on a runner with no X server.
|
||||||
|
#
|
||||||
|
# A build-system knob would not just be redundant, it would be a trap: `set(...
|
||||||
|
# CACHE ...)` does not rewrite an existing cache, so every build directory
|
||||||
|
# configured before this change would keep injecting EGL_PLATFORM=x11 and go on
|
||||||
|
# binding to a window system - silently, and only on the machines that have one.
|
||||||
|
# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own
|
||||||
|
# environment, which the harness still honours.
|
||||||
|
|
||||||
|
set(MGL_ITEST_COMMON_ENV "")
|
||||||
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
|
endif()
|
||||||
|
unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11
|
||||||
|
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
||||||
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
|
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
||||||
|
# forwards PROPERTIES as a flat list - so a plain `;`-joined value arrives as
|
||||||
|
# four separate arguments and everything after the first is silently read as
|
||||||
|
# another property name. Escaping the separators keeps the whole thing one list
|
||||||
|
# element until set_tests_properties expands it back. Without this only
|
||||||
|
# MOBILEGL_BACKEND_TYPE reaches the test and the vendor/ICD pinning is lost.
|
||||||
|
function(mgl_itest_join_environment outVar)
|
||||||
|
set(joined "")
|
||||||
|
foreach(entry IN LISTS ARGN)
|
||||||
|
if (joined)
|
||||||
|
string(APPEND joined "\\;${entry}")
|
||||||
|
else()
|
||||||
|
set(joined "${entry}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
set(${outVar} "${joined}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
|
||||||
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
|
||||||
|
# Discovery runs `--gtest_list_tests`, which does not construct the harness and
|
||||||
|
# so needs no GPU. One registration per backend; TEST_PREFIX keeps the two sets
|
||||||
|
# of ctest names apart.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES."
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan."
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A third registration, of ONE scenario, with asynchronous shader compilation
|
||||||
|
# pinned on. Not a second code path in the renderer: a second ALLOCATION pattern.
|
||||||
|
# The async pipeline's job objects change which of the freed blocks the capture
|
||||||
|
# phase is handed, and that is what decides whether the destroyed-VAO address is
|
||||||
|
# reached at all - on the ablated (pre-fix) tree async=1 reproduced 3 runs out of
|
||||||
|
# 3 where the ambient default reproduced 2 of 3. Pinning it here means the
|
||||||
|
# high-signal configuration runs whatever the shipped default becomes, instead of
|
||||||
|
# the suite quietly weakening the day that default flips. It must be process-wide
|
||||||
|
# (the ENVIRONMENT property), not an in-process scope: the compile pool and its
|
||||||
|
# threads are stood up at initialization, and their allocations are half the
|
||||||
|
# point. DirectVulkan only - the memo this pins is DirectVulkan's.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan.AsyncCompile."
|
||||||
|
TEST_FILTER "XfbAfterClipDistanceScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A fourth registration, of the depth/stencil readback scenarios, with the ES
|
||||||
|
# shader-sampling emulation forced on. Not paranoia - without it these scenarios are
|
||||||
|
# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil
|
||||||
|
# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them
|
||||||
|
# goes green through a native path that the Adreno device does not have. Deleting the
|
||||||
|
# entire emulation left all of them passing. With the flag the native spellings are off
|
||||||
|
# the table and only the path the device actually takes remains. DirectGLES only - the
|
||||||
|
# emulation is DirectGLES's.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation."
|
||||||
|
TEST_FILTER "DepthStencilReadback*Scenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,761 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "HeadlessGL.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
#include <android/hardware_buffer.h>
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <media/NdkImage.h>
|
||||||
|
#include <media/NdkImageReader.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
|
||||||
|
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
||||||
|
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
||||||
|
// to a system loader.
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
// The pre-flight below runs the whole EGL bring-up in a forked child, which is
|
||||||
|
// the only construction that is actually predictive here: MobileGL ABORTS
|
||||||
|
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
||||||
|
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
||||||
|
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||||
|
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||||
|
#include <csignal>
|
||||||
|
#include <ctime>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#else
|
||||||
|
#define MGITEST_HAVE_FORK_PREFLIGHT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Small enough that a readback is cheap, big enough that "top third" and
|
||||||
|
// "bottom third" are unambiguous. Non-square on purpose: a transposing
|
||||||
|
// bug cannot hide behind a square.
|
||||||
|
constexpr int kSurfaceWidth = 128;
|
||||||
|
constexpr int kSurfaceHeight = 96;
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
HWND g_testWindow = nullptr;
|
||||||
|
|
||||||
|
HWND CreateTestWindow() {
|
||||||
|
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
|
||||||
|
static bool registered = false;
|
||||||
|
if (!registered) {
|
||||||
|
WNDCLASSW windowClass{};
|
||||||
|
windowClass.lpfnWndProc = DefWindowProcW;
|
||||||
|
windowClass.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
windowClass.lpszClassName = kClassName;
|
||||||
|
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
|
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
|
||||||
|
GetModuleHandleW(nullptr), nullptr);
|
||||||
|
}
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
AImageReader* g_imageReader = nullptr;
|
||||||
|
ANativeWindow* g_imageReaderWindow = nullptr;
|
||||||
|
|
||||||
|
void DrainImageReader(void*, AImageReader* reader) {
|
||||||
|
AImage* image = nullptr;
|
||||||
|
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
|
||||||
|
AImage_delete(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CreateImageReaderWindow() {
|
||||||
|
if (g_imageReaderWindow != nullptr) return true;
|
||||||
|
constexpr int kMaxImages = 4;
|
||||||
|
const media_status_t status = AImageReader_newWithUsage(
|
||||||
|
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
||||||
|
kMaxImages, &g_imageReader);
|
||||||
|
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
|
||||||
|
|
||||||
|
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
|
||||||
|
AImageReader_setImageListener(g_imageReader, &listener);
|
||||||
|
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
|
||||||
|
g_imageReaderWindow == nullptr) {
|
||||||
|
AImageReader_setImageListener(g_imageReader, nullptr);
|
||||||
|
AImageReader_delete(g_imageReader);
|
||||||
|
g_imageReader = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ANativeWindow_acquire(g_imageReaderWindow);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyImageReaderWindow() {
|
||||||
|
if (g_imageReaderWindow != nullptr) {
|
||||||
|
ANativeWindow_release(g_imageReaderWindow);
|
||||||
|
g_imageReaderWindow = nullptr;
|
||||||
|
}
|
||||||
|
if (g_imageReader != nullptr) {
|
||||||
|
AImageReader_setImageListener(g_imageReader, nullptr);
|
||||||
|
AImageReader_delete(g_imageReader);
|
||||||
|
g_imageReader = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool UseWindowSurface() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
|
||||||
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string EnvOr(const char* name, const char* fallback) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A skip reason is only useful if it says which call failed AND why, so
|
||||||
|
// every bring-up step reports the EGL error it left behind.
|
||||||
|
std::string WithEglError(const char* what) {
|
||||||
|
std::ostringstream out;
|
||||||
|
out << what << " (eglGetError=0x" << std::hex << eglGetError() << ")";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The EGL objects one bring-up produces.
|
||||||
|
struct EglBringUp {
|
||||||
|
void* display = nullptr;
|
||||||
|
void* surface = nullptr;
|
||||||
|
void* context = nullptr;
|
||||||
|
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 on desktop. Android instead supplies an AImageReader
|
||||||
|
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||||
|
// driver that consults them directly cannot reintroduce the dependency
|
||||||
|
// behind EGL's back.
|
||||||
|
void EnsureHeadlessPlatform() {
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
static bool done = false;
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
// An explicit EGL_PLATFORM from the operator still wins: pinning a
|
||||||
|
// platform is exactly how someone reproduces a platform-specific bug.
|
||||||
|
if (std::getenv("EGL_PLATFORM") == nullptr) {
|
||||||
|
setenv("EGL_PLATFORM", "surfaceless", 1);
|
||||||
|
}
|
||||||
|
unsetenv("DISPLAY");
|
||||||
|
unsetenv("WAYLAND_DISPLAY");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE bring-up, in one function so the pre-flight child and the parent run
|
||||||
|
// literally the same sequence - a pre-flight that tests something narrower
|
||||||
|
// than what the parent will do is exactly the kind of "predictive" check
|
||||||
|
// that is not.
|
||||||
|
//
|
||||||
|
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||||
|
// fills outReason either way.
|
||||||
|
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
||||||
|
// Belt and braces: the pre-flight child and the parent both enter here,
|
||||||
|
// and neither may be the first to touch EGL without this having run.
|
||||||
|
EnsureHeadlessPlatform();
|
||||||
|
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (display == EGL_NO_DISPLAY) {
|
||||||
|
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
EGLint major = 0, minor = 0;
|
||||||
|
if (eglInitialize(display, &major, &minor) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglInitialize failed: no usable display/driver on this machine");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglBindAPI(EGL_OPENGL_API) failed");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool useWindowSurface = UseWindowSurface();
|
||||||
|
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
||||||
|
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
|
||||||
|
EGL_RED_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_GREEN_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_BLUE_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_ALPHA_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_DEPTH_SIZE,
|
||||||
|
24,
|
||||||
|
EGL_RENDERABLE_TYPE,
|
||||||
|
EGL_OPENGL_BIT,
|
||||||
|
EGL_NONE};
|
||||||
|
EGLConfig config = nullptr;
|
||||||
|
EGLint configCount = 0;
|
||||||
|
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
||||||
|
outReason = WithEglError(useWindowSurface
|
||||||
|
? "eglChooseConfig found no window-capable RGBA8/D24 config"
|
||||||
|
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
|
||||||
|
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
context = eglCreateContext(display, config, EGL_NO_CONTEXT, nullptr);
|
||||||
|
}
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
outReason = WithEglError("eglCreateContext failed: no desktop-GL context available");
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLSurface surface = EGL_NO_SURFACE;
|
||||||
|
if (useWindowSurface) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
|
||||||
|
if (g_testWindow == nullptr) {
|
||||||
|
outReason = "failed to create the Windows integration-test window";
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
if (!CreateImageReaderWindow()) {
|
||||||
|
outReason = "failed to create the Android AImageReader integration-test window";
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||||
|
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||||
|
}
|
||||||
|
if (surface == EGL_NO_SURFACE) {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
DestroyImageReaderWindow();
|
||||||
|
#endif
|
||||||
|
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
||||||
|
: "eglCreatePbufferSurface failed");
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
// The step that brings the whole backend up (DirectVulkan creates its
|
||||||
|
// instance, device and surface in here) and therefore the step that
|
||||||
|
// aborts instead of returning an error on an unusable platform.
|
||||||
|
if (eglMakeCurrent(display, surface, surface, context) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglMakeCurrent failed");
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||||
|
if (renderer == nullptr) {
|
||||||
|
outReason = "glGetString(GL_RENDERER) returned null after eglMakeCurrent";
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.display = display;
|
||||||
|
out.surface = surface;
|
||||||
|
out.context = context;
|
||||||
|
out.renderer = reinterpret_cast<const char*>(renderer);
|
||||||
|
outReason.clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform pre-flight, and the reason this module can claim to skip
|
||||||
|
// cleanly rather than merely hope to.
|
||||||
|
//
|
||||||
|
// MobileGL does not return errors when the platform is unusable - it
|
||||||
|
// ABORTS. MOBILEGL_ASSERT raises SIGTRAP, and the DirectVulkan bring-up
|
||||||
|
// asserts its way through instance, physical-device and surface creation
|
||||||
|
// inside eglMakeCurrent. So there is no in-process question the harness
|
||||||
|
// can ask that is guaranteed to be survivable, and the old form (dlopen
|
||||||
|
// the Vulkan loader, count physical devices, look for
|
||||||
|
// VK_EXT_headless_surface) was a guess at the abort conditions rather
|
||||||
|
// than a test of them: it named three of the ways bring-up can die and
|
||||||
|
// was silent about every other one, including every DirectGLES one.
|
||||||
|
//
|
||||||
|
// What is actually predictive is to run the bring-up itself somewhere a
|
||||||
|
// SIGTRAP is a datum instead of a crash. fork() gives exactly that: the
|
||||||
|
// child performs the identical sequence and _exit(0)s on success, and
|
||||||
|
// ANY non-zero exit or ANY signal in the parent's waitpid() means "this
|
||||||
|
// platform is unusable" - whatever the reason, including reasons nobody
|
||||||
|
// has thought of. Only then does the parent do the real bring-up.
|
||||||
|
//
|
||||||
|
// Returns an empty string when the platform survived a full bring-up.
|
||||||
|
std::string PreflightBringUp() {
|
||||||
|
#if !MGITEST_HAVE_FORK_PREFLIGHT
|
||||||
|
// No fork(): let the in-process bring-up speak for itself, which is
|
||||||
|
// what this module did before. Windows/macOS are not CI targets for
|
||||||
|
// the headless scenarios.
|
||||||
|
return {};
|
||||||
|
#else
|
||||||
|
int channel[2] = {-1, -1};
|
||||||
|
if (pipe(channel) != 0) {
|
||||||
|
return {}; // cannot pre-flight; fall through to the in-process attempt
|
||||||
|
}
|
||||||
|
// The child inherits our stdio buffers; flush so nothing is printed twice.
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t child = fork();
|
||||||
|
if (child < 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
close(channel[1]);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (child == 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
// No core suppression here, deliberately: when the child dies on a
|
||||||
|
// signal, the core IS the diagnosis (an rlimit that used to sit here
|
||||||
|
// made a CI-only crash undebuggable). Machines that do not want
|
||||||
|
// cores control that with the usual ulimit/core_pattern knobs.
|
||||||
|
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||||
|
EglBringUp local;
|
||||||
|
std::string reason;
|
||||||
|
const int step = RunEglBringUp(local, reason);
|
||||||
|
if (!reason.empty()) {
|
||||||
|
const std::size_t bytes = std::min<std::size_t>(reason.size(), 480);
|
||||||
|
const ssize_t written = write(channel[1], reason.data(), bytes);
|
||||||
|
(void)written;
|
||||||
|
}
|
||||||
|
close(channel[1]);
|
||||||
|
// _exit, never exit(): every atexit handler and static destructor
|
||||||
|
// in this address space belongs to the parent's copy of the world,
|
||||||
|
// and the child is holding a live context it must not tear down.
|
||||||
|
_exit(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(channel[1]);
|
||||||
|
// Reap first, read after: the message is bounded well below the pipe
|
||||||
|
// buffer so the child can never block writing it, and polling the exit
|
||||||
|
// status is what lets a wedged child be killed instead of hanging the
|
||||||
|
// parent on a read that will never return.
|
||||||
|
constexpr int kPreflightTimeoutMs = 30000;
|
||||||
|
int status = 0;
|
||||||
|
int waitedMs = 0;
|
||||||
|
for (;;) {
|
||||||
|
const pid_t reaped = waitpid(child, &status, WNOHANG);
|
||||||
|
if (reaped == child) break;
|
||||||
|
if (reaped < 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
return "waitpid on the EGL bring-up pre-flight child failed";
|
||||||
|
}
|
||||||
|
if (waitedMs >= kPreflightTimeoutMs) {
|
||||||
|
kill(child, SIGKILL);
|
||||||
|
(void)waitpid(child, &status, 0);
|
||||||
|
close(channel[0]);
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "the EGL bring-up wedged: a forked pre-flight child made no progress in "
|
||||||
|
<< kPreflightTimeoutMs / 1000 << "s and was killed";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
timespec nap{0, 10 * 1000 * 1000};
|
||||||
|
nanosleep(&nap, nullptr);
|
||||||
|
waitedMs += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string childSays;
|
||||||
|
char buffer[512];
|
||||||
|
for (;;) {
|
||||||
|
const ssize_t got = read(channel[0], buffer, sizeof(buffer));
|
||||||
|
if (got <= 0) break;
|
||||||
|
childSays.append(buffer, static_cast<std::size_t>(got));
|
||||||
|
}
|
||||||
|
close(channel[0]);
|
||||||
|
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
const int signalNumber = WTERMSIG(status);
|
||||||
|
const char* signalName = strsignal(signalNumber);
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "the EGL bring-up ABORTS on this platform: a forked pre-flight child died on signal "
|
||||||
|
<< signalNumber << " (" << (signalName != nullptr ? signalName : "?") << ")";
|
||||||
|
if (!childSays.empty()) out << " after: " << childSays;
|
||||||
|
out << ". MobileGL asserts rather than returning an error here, so the scenarios would "
|
||||||
|
"have taken the whole test binary down with them";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
if (!WIFEXITED(status)) {
|
||||||
|
return "the EGL bring-up pre-flight child neither exited nor was signalled";
|
||||||
|
}
|
||||||
|
const int exitStatus = WEXITSTATUS(status);
|
||||||
|
if (exitStatus != 0) {
|
||||||
|
std::ostringstream out;
|
||||||
|
out << (childSays.empty() ? "the EGL bring-up failed" : childSays)
|
||||||
|
<< " (forked pre-flight child exit status " << exitStatus << ")";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // 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() {
|
||||||
|
return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RequireHardwareGpu() {
|
||||||
|
return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||||
|
os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba8 Image::At(int x, int y) const {
|
||||||
|
if (x < 0 || y < 0 || x >= m_width || y >= m_height) {
|
||||||
|
return Rgba8{};
|
||||||
|
}
|
||||||
|
const std::size_t index = (static_cast<std::size_t>(y) * m_width + x) * 4;
|
||||||
|
return Rgba8{m_pixels[index], m_pixels[index + 1], m_pixels[index + 2], m_pixels[index + 3]};
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* Image::ColorName(int x, int y) const {
|
||||||
|
const Rgba8 c = At(x, y);
|
||||||
|
const bool r = c.r > 160, g = c.g > 160, b = c.b > 160;
|
||||||
|
const bool nr = c.r < 96, ng = c.g < 96, nb = c.b < 96;
|
||||||
|
if (nr && ng && nb) return "black";
|
||||||
|
if (r && g && b) return "white";
|
||||||
|
if (r && ng && nb) return "red";
|
||||||
|
if (nr && g && nb) return "green";
|
||||||
|
if (nr && ng && b) return "blue";
|
||||||
|
if (r && g && nb) return "yellow";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Image::ByteDiffCount(const Image& other) const {
|
||||||
|
if (m_width != other.m_width || m_height != other.m_height) {
|
||||||
|
return std::max(m_pixels.size(), other.m_pixels.size());
|
||||||
|
}
|
||||||
|
std::size_t differing = 0;
|
||||||
|
for (std::size_t i = 0; i < m_pixels.size(); ++i) {
|
||||||
|
if (m_pixels[i] != other.m_pixels[i]) ++differing;
|
||||||
|
}
|
||||||
|
return differing;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Image::QuadrantSignature() const {
|
||||||
|
if (m_width < 2 || m_height < 2) return "<empty>";
|
||||||
|
// Quadrant CENTRES, so a one-pixel rounding difference at a quadrant edge
|
||||||
|
// never decides the answer. Order is fixed and load-bearing: bottom-left,
|
||||||
|
// bottom-right, top-left, top-right.
|
||||||
|
const int leftX = m_width / 4;
|
||||||
|
const int rightX = m_width * 3 / 4;
|
||||||
|
const int bottomY = m_height / 4;
|
||||||
|
const int topY = m_height * 3 / 4;
|
||||||
|
std::ostringstream out;
|
||||||
|
out << ColorName(leftX, bottomY) << "," << ColorName(rightX, bottomY) << "," << ColorName(leftX, topY) << ","
|
||||||
|
<< ColorName(rightX, topY);
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionScan ScanRegion(const Image& image, int x0, int x1, int y0, int y1, const char* expectedColor) {
|
||||||
|
RegionScan scan;
|
||||||
|
x0 = std::max(x0, 0);
|
||||||
|
y0 = std::max(y0, 0);
|
||||||
|
x1 = std::min(x1, image.Width() - 1);
|
||||||
|
y1 = std::min(y1, image.Height() - 1);
|
||||||
|
for (int y = y0; y <= y1; ++y) {
|
||||||
|
for (int x = x0; x <= x1; ++x) {
|
||||||
|
++scan.total;
|
||||||
|
const char* name = image.ColorName(x, y);
|
||||||
|
if (std::strcmp(name, expectedColor) == 0) continue;
|
||||||
|
++scan.offenders;
|
||||||
|
if (scan.firstX < 0) {
|
||||||
|
scan.firstX = x;
|
||||||
|
scan.firstY = y;
|
||||||
|
scan.firstColor = image.At(x, y);
|
||||||
|
scan.firstColorName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scan;
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::AssertionResult RegionIsMostly(const Image& image, int x0, int x1, int y0, int y1,
|
||||||
|
const char* expectedColor, double tolerance,
|
||||||
|
const std::string& when) {
|
||||||
|
const RegionScan scan = ScanRegion(image, x0, x1, y0, y1, expectedColor);
|
||||||
|
if (scan.total == 0) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< when << ": region x[" << x0 << "," << x1 << "] y[" << y0 << "," << y1
|
||||||
|
<< "] is empty against a " << image.Width() << "x" << image.Height() << " readback";
|
||||||
|
}
|
||||||
|
const double offendingFraction = static_cast<double>(scan.offenders) / scan.total;
|
||||||
|
if (offendingFraction <= tolerance) {
|
||||||
|
return ::testing::AssertionSuccess();
|
||||||
|
}
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< when << ": region x[" << x0 << "," << x1 << "] y[" << y0 << "," << y1 << "] should be all "
|
||||||
|
<< expectedColor << ", but " << scan.offenders << " of " << scan.total << " pixels ("
|
||||||
|
<< static_cast<int>(offendingFraction * 100.0 + 0.5) << "%) are not; first offender at (" << scan.firstX
|
||||||
|
<< "," << scan.firstY << ") is " << scan.firstColorName << " " << scan.firstColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessGL& HeadlessGL::Get() {
|
||||||
|
static HeadlessGL instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessGL::HeadlessGL() {
|
||||||
|
// Before anything else in this process can reach EGL, and in particular
|
||||||
|
// before the pre-flight forks - the child must measure the same platform
|
||||||
|
// the parent will use.
|
||||||
|
EnsureHeadlessPlatform();
|
||||||
|
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||||
|
m_usable = BringUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HeadlessGL::BringUp() {
|
||||||
|
// Ask a disposable copy of this process first. Only if it survived does
|
||||||
|
// the real one try - see PreflightBringUp for why nothing weaker is
|
||||||
|
// predictive against a stack that aborts instead of returning errors.
|
||||||
|
const std::string preflightProblem = PreflightBringUp();
|
||||||
|
if (!preflightProblem.empty()) {
|
||||||
|
m_skipReason = preflightProblem;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same shape as DriverBench's boot_egl(), minus the dlopen: the provider
|
||||||
|
// is this binary. A pbuffer needs no window system, but MobileGL's own
|
||||||
|
// loader still has to reach a real driver underneath - and the child
|
||||||
|
// above just proved it can.
|
||||||
|
EglBringUp brought;
|
||||||
|
std::string reason;
|
||||||
|
if (RunEglBringUp(brought, reason) != 0) {
|
||||||
|
// The pre-flight passed and the parent's identical attempt did not.
|
||||||
|
// That is a real result, not a machine without a GPU, so say so: it
|
||||||
|
// means something is different between the two attempts (a leaked
|
||||||
|
// exclusive device, an environment the child did not have).
|
||||||
|
m_skipReason = reason + " - although an identical bring-up in a forked pre-flight child succeeded";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_display = brought.display;
|
||||||
|
m_surface = brought.surface;
|
||||||
|
m_context = brought.context;
|
||||||
|
m_width = kSurfaceWidth;
|
||||||
|
m_height = kSurfaceHeight;
|
||||||
|
m_renderer = std::move(brought.renderer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessGL::EndFrame() {
|
||||||
|
if (!m_usable) return;
|
||||||
|
eglSwapBuffers(static_cast<EGLDisplay>(m_display), static_cast<EGLSurface>(m_surface));
|
||||||
|
++m_frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessGL::ShutDown() {
|
||||||
|
if (!m_usable) return;
|
||||||
|
EGLDisplay display = static_cast<EGLDisplay>(m_display);
|
||||||
|
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
||||||
|
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
||||||
|
eglTerminate(display);
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (g_testWindow != nullptr) {
|
||||||
|
DestroyWindow(g_testWindow);
|
||||||
|
g_testWindow = nullptr;
|
||||||
|
}
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
DestroyImageReaderWindow();
|
||||||
|
#endif
|
||||||
|
m_context = nullptr;
|
||||||
|
m_surface = nullptr;
|
||||||
|
m_display = nullptr;
|
||||||
|
m_usable = false;
|
||||||
|
m_skipReason = "the headless context has already been torn down";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- scenario vocabulary ------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
unsigned int CompileStage(GLenum stage, const char* source, std::string* outError) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, &length, log);
|
||||||
|
if (outError != nullptr) {
|
||||||
|
*outError = std::string(stage == GL_VERTEX_SHADER ? "vertex" : "fragment") +
|
||||||
|
" shader failed to compile: " + log;
|
||||||
|
}
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
unsigned int CompileProgram(const char* vertexSource, const char* fragmentSource, std::string* outError) {
|
||||||
|
const GLuint vs = CompileStage(GL_VERTEX_SHADER, vertexSource, outError);
|
||||||
|
if (vs == 0) return 0;
|
||||||
|
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource, outError);
|
||||||
|
if (fs == 0) {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
// Pinned rather than queried so the scenarios can set up a VAO without a
|
||||||
|
// round trip, and so a driver that reorders attributes cannot change what
|
||||||
|
// the test means.
|
||||||
|
glBindAttribLocation(program, 0, "aPos");
|
||||||
|
glBindAttribLocation(program, 1, "aColor");
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, &length, log);
|
||||||
|
if (outError != nullptr) *outError = std::string("program failed to link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorFbo MakeColorFbo(int width, int height) {
|
||||||
|
ColorFbo target;
|
||||||
|
target.width = width;
|
||||||
|
target.height = height;
|
||||||
|
glGenTextures(1, &target.texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
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);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenFramebuffers(1, &target.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyColorFbo(ColorFbo& target) {
|
||||||
|
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||||
|
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||||
|
target.fbo = 0;
|
||||||
|
target.texture = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindDefaultFramebuffer() {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindFbo(const ColorFbo& target) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glViewport(0, 0, target.width, target.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearTo(float r, float g, float b, float a) {
|
||||||
|
glClearColor(r, g, b, a);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image ReadPixels(int width, int height) {
|
||||||
|
return ReadPixelsRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image ReadPixelsRect(int x, int y, int width, int height) {
|
||||||
|
Image image(width, height);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int FirstGLError() {
|
||||||
|
const GLenum first = glGetError();
|
||||||
|
if (first == GL_NO_ERROR) return GL_NO_ERROR;
|
||||||
|
// Drain, bounded: a broken stack must not turn an error check into a hang.
|
||||||
|
for (int i = 0; i < 64 && glGetError() != GL_NO_ERROR; ++i) {}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* GLErrorName(unsigned int error) {
|
||||||
|
switch (error) {
|
||||||
|
case GL_NO_ERROR:
|
||||||
|
return "GL_NO_ERROR";
|
||||||
|
case GL_INVALID_ENUM:
|
||||||
|
return "GL_INVALID_ENUM";
|
||||||
|
case GL_INVALID_VALUE:
|
||||||
|
return "GL_INVALID_VALUE";
|
||||||
|
case GL_INVALID_OPERATION:
|
||||||
|
return "GL_INVALID_OPERATION";
|
||||||
|
case GL_OUT_OF_MEMORY:
|
||||||
|
return "GL_OUT_OF_MEMORY";
|
||||||
|
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
||||||
|
return "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||||||
|
default:
|
||||||
|
return "GL_<unknown>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// A headless GL context and the small vocabulary the scenarios are written in.
|
||||||
|
//
|
||||||
|
// The scenarios in this module are end-to-end: they drive MobileGL's own GL and
|
||||||
|
// EGL entry points (this binary links MobileGL_s, so gl*/egl* resolve straight
|
||||||
|
// into the implementation) and assert on glReadPixels output. Nothing here
|
||||||
|
// inspects backend state - both bugs this module pins were invisible to
|
||||||
|
// state-level assertions and visible only in pixels.
|
||||||
|
//
|
||||||
|
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
|
||||||
|
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
|
||||||
|
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
|
||||||
|
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
|
||||||
|
// boundary the cross-frame scenarios need to be real).
|
||||||
|
//
|
||||||
|
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||||
|
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||||
|
// than trying to switch in-process.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// True when MOBILEGL_ITEST_REQUIRE_GPU is set in the environment: the runner
|
||||||
|
// is asserting that this machine HAS a usable GPU, so "no GPU" stops being a
|
||||||
|
// clean skip and becomes a failure. Without it the integration-gpu label is
|
||||||
|
// unfalsifiable - a CI job that ran nothing reports exactly the same green as
|
||||||
|
// a job that ran everything.
|
||||||
|
bool RequireGpu();
|
||||||
|
|
||||||
|
// True when MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set: additionally asserts
|
||||||
|
// that the context did NOT land on a software rasterizer. Deliberately a
|
||||||
|
// SEPARATE switch from RequireGpu - a GPU-less CI runner is a supported and
|
||||||
|
// intended configuration for these scenarios (they pin backend draw logic,
|
||||||
|
// which llvmpipe/lavapipe execute faithfully), so CI wants the falsifiability
|
||||||
|
// of REQUIRE_GPU without the hardware demand. Use this one only where a vendor
|
||||||
|
// pin silently degrading to software would invalidate the measurement.
|
||||||
|
bool RequireHardwareGpu();
|
||||||
|
|
||||||
|
struct Rgba8 {
|
||||||
|
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
||||||
|
|
||||||
|
bool operator==(const Rgba8& other) const {
|
||||||
|
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||||
|
}
|
||||||
|
bool operator!=(const Rgba8& other) const { return !(*this == other); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prints as "rgba(255,0,0,255)" so a gtest failure names the colour it saw.
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c);
|
||||||
|
|
||||||
|
// An RGBA8 readback. Row 0 is the BOTTOM row: that is GL's convention for
|
||||||
|
// glReadPixels and it is what "correctly oriented" means everywhere below.
|
||||||
|
class Image {
|
||||||
|
public:
|
||||||
|
Image() = default;
|
||||||
|
Image(int width, int height)
|
||||||
|
: m_width(width), m_height(height), m_pixels(static_cast<std::size_t>(width) * height * 4, 0) {}
|
||||||
|
|
||||||
|
int Width() const { return m_width; }
|
||||||
|
int Height() const { return m_height; }
|
||||||
|
bool Empty() const { return m_pixels.empty(); }
|
||||||
|
std::uint8_t* Data() { return m_pixels.data(); }
|
||||||
|
const std::uint8_t* Data() const { return m_pixels.data(); }
|
||||||
|
|
||||||
|
Rgba8 At(int x, int y) const;
|
||||||
|
// Nearest of {black, red, green, blue, white, other} - the scenarios only
|
||||||
|
// ever draw those, so this turns a pixel into something readable.
|
||||||
|
const char* ColorName(int x, int y) const;
|
||||||
|
|
||||||
|
bool operator==(const Image& other) const {
|
||||||
|
return m_width == other.m_width && m_height == other.m_height && m_pixels == other.m_pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count of differing bytes, for a failure message that says how wrong.
|
||||||
|
std::size_t ByteDiffCount(const Image& other) const;
|
||||||
|
|
||||||
|
// The four quadrant centres, in the fixed order
|
||||||
|
// bottom-left, bottom-right, top-left, top-right.
|
||||||
|
//
|
||||||
|
// This replaces the old VerticalSignature(bandCount), which read three
|
||||||
|
// full-width horizontal stripes down the centre line and was therefore
|
||||||
|
// blind to an X flip, to a transpose, and to a 180 rotation composed with
|
||||||
|
// a Y flip - all of those left the stripe order alone. Four quadrant
|
||||||
|
// colours are asymmetric in BOTH axes, so each of the eight square
|
||||||
|
// symmetries produces a different string (see OrientationScenario, which
|
||||||
|
// spells all eight out).
|
||||||
|
std::string QuadrantSignature() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_width = 0;
|
||||||
|
int m_height = 0;
|
||||||
|
std::vector<std::uint8_t> m_pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The process-wide headless context. Brought up lazily on the first Get() so
|
||||||
|
// that `--gtest_list_tests` (which CMake runs at build time to discover the
|
||||||
|
// cases) never touches a GPU.
|
||||||
|
class HeadlessGL {
|
||||||
|
public:
|
||||||
|
static HeadlessGL& Get();
|
||||||
|
|
||||||
|
// False on a machine with no usable GPU/display/ICD. SkipReason() then
|
||||||
|
// says which step failed; every fixture turns that into GTEST_SKIP().
|
||||||
|
bool Usable() const { return m_usable; }
|
||||||
|
const std::string& SkipReason() const { return m_skipReason; }
|
||||||
|
|
||||||
|
// Backend actually in use, as reported by MOBILEGL_BACKEND_TYPE.
|
||||||
|
const std::string& BackendName() const { return m_backendName; }
|
||||||
|
const std::string& RendererString() const { return m_renderer; }
|
||||||
|
|
||||||
|
int Width() const { return m_width; }
|
||||||
|
int Height() const { return m_height; }
|
||||||
|
|
||||||
|
// THE frame boundary. eglSwapBuffers is what retires a frame in the
|
||||||
|
// renderer, and the cross-frame scenarios are meaningless without it.
|
||||||
|
void EndFrame();
|
||||||
|
|
||||||
|
// Frames completed so far, for failure messages.
|
||||||
|
int FrameIndex() const { return m_frameIndex; }
|
||||||
|
|
||||||
|
// Releases the context and surface and terminates the display. Called
|
||||||
|
// once, after the last scenario: MobileGL frees its backend objects
|
||||||
|
// through eglTerminate, and letting a process simply exit on top of a
|
||||||
|
// live context leaves those objects to be torn down from a static
|
||||||
|
// destructor with no driver left underneath.
|
||||||
|
void ShutDown();
|
||||||
|
|
||||||
|
private:
|
||||||
|
HeadlessGL();
|
||||||
|
HeadlessGL(const HeadlessGL&) = delete;
|
||||||
|
HeadlessGL& operator=(const HeadlessGL&) = delete;
|
||||||
|
|
||||||
|
bool BringUp();
|
||||||
|
|
||||||
|
bool m_usable = false;
|
||||||
|
std::string m_skipReason;
|
||||||
|
std::string m_backendName;
|
||||||
|
std::string m_renderer;
|
||||||
|
int m_width = 0;
|
||||||
|
int m_height = 0;
|
||||||
|
int m_frameIndex = 0;
|
||||||
|
void* m_display = nullptr;
|
||||||
|
void* m_surface = nullptr;
|
||||||
|
void* m_context = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the scenario vocabulary -------------------------------------------
|
||||||
|
// Deliberately tiny. A scenario should read like a story; anything that
|
||||||
|
// needs a comment about GL mechanics belongs here instead.
|
||||||
|
|
||||||
|
// Compiles and links vs+fs, pinning attribute 0 to "aPos" and 1 to "aColor".
|
||||||
|
// Returns 0 and fills outError on failure.
|
||||||
|
unsigned int CompileProgram(const char* vertexSource, const char* fragmentSource, std::string* outError);
|
||||||
|
|
||||||
|
struct ColorFbo {
|
||||||
|
unsigned int fbo = 0;
|
||||||
|
unsigned int texture = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A complete RGBA8 render target. Returns fbo==0 on failure.
|
||||||
|
ColorFbo MakeColorFbo(int width, int height);
|
||||||
|
void DestroyColorFbo(ColorFbo& target);
|
||||||
|
|
||||||
|
// Binds a target and sets the viewport to match. Passing fbo 0 means the
|
||||||
|
// default (presentable) framebuffer.
|
||||||
|
void BindDefaultFramebuffer();
|
||||||
|
void BindFbo(const ColorFbo& target);
|
||||||
|
|
||||||
|
void ClearTo(float r, float g, float b, float a);
|
||||||
|
|
||||||
|
// Reads back the whole currently bound READ framebuffer.
|
||||||
|
Image ReadPixels(int width, int height);
|
||||||
|
|
||||||
|
// A PARTIAL glReadPixels. Row 0 of the returned image is GL row `y` of the
|
||||||
|
// framebuffer, i.e. the bottom row of the requested rect - the same
|
||||||
|
// convention ReadPixels uses, just with an origin. This is the shape the
|
||||||
|
// conformance suite reads in (a random sub-rect of the default
|
||||||
|
// framebuffer), and the shape DirectVulkan's default-FBO readback used to
|
||||||
|
// hand back in Vulkan row order because its re-orientation only ran on an
|
||||||
|
// exact full-extent read.
|
||||||
|
Image ReadPixelsRect(int x, int y, int width, int height);
|
||||||
|
|
||||||
|
// Drains any GL error queue and returns the first error, or 0.
|
||||||
|
unsigned int FirstGLError();
|
||||||
|
const char* GLErrorName(unsigned int error);
|
||||||
|
|
||||||
|
// ---- whole-region readback predicates ----------------------------------
|
||||||
|
// The scenarios used to assert on two or three individual pixels, which is
|
||||||
|
// provably too weak: a draw in which 3 of a quad's 4 vertices carry stale
|
||||||
|
// data still paints the sampled centre the expected colour (that exact case
|
||||||
|
// is a standing negative-control test - see CrossFrameBufferScenario). The
|
||||||
|
// readback is already fully in memory, so counting every pixel in a region
|
||||||
|
// costs nothing and turns "the middle looks right" into "all of it is right".
|
||||||
|
|
||||||
|
// Everything a caller needs to say what was wrong and where.
|
||||||
|
struct RegionScan {
|
||||||
|
int total = 0; // pixels examined
|
||||||
|
int offenders = 0; // pixels whose ColorName() != expected
|
||||||
|
int firstX = -1; // first offender in bottom-to-top, left-to-right order
|
||||||
|
int firstY = -1;
|
||||||
|
Rgba8 firstColor{};
|
||||||
|
std::string firstColorName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inclusive pixel bounds, clamped to the image. Row 0 is the bottom row.
|
||||||
|
RegionScan ScanRegion(const Image& image, int x0, int x1, int y0, int y1, const char* expectedColor);
|
||||||
|
|
||||||
|
// gtest predicate wrapper: EXPECT_TRUE(RegionIsMostly(...)) reports the
|
||||||
|
// offender count, the offender fraction and the FIRST offending pixel's
|
||||||
|
// coordinates and colour. `tolerance` is the fraction of the region allowed
|
||||||
|
// to disagree; pass 0.0 to demand every pixel (which is what the scenarios
|
||||||
|
// do - they inset their regions away from primitive edges so exactness is
|
||||||
|
// achievable).
|
||||||
|
::testing::AssertionResult RegionIsMostly(const Image& image, int x0, int x1, int y0, int y1,
|
||||||
|
const char* expectedColor, double tolerance,
|
||||||
|
const std::string& when);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The base fixture every scenario derives from. Its only jobs are to bring the
|
||||||
|
// headless context up once per process and to decide what "this machine has no
|
||||||
|
// usable GPU" means.
|
||||||
|
//
|
||||||
|
// By default it means a clean GTEST_SKIP() - never a failure, never a hang -
|
||||||
|
// because a developer box or a container without a GPU should not fail a run it
|
||||||
|
// was never able to perform. But a skip is indistinguishable from a pass in
|
||||||
|
// every CI summary, so the `integration-gpu` label on its own is unfalsifiable:
|
||||||
|
// a runner whose driver pinning silently broke reports the same green as one
|
||||||
|
// that rendered every frame. MOBILEGL_ITEST_REQUIRE_GPU is the caller saying
|
||||||
|
// "this machine HAS a GPU and I am relying on these scenarios actually running";
|
||||||
|
// with it set, an unusable harness is a FAILURE carrying the pre-flight's reason.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "HeadlessGL.h"
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
class ScenarioTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
m_ready = false;
|
||||||
|
HeadlessGL& gl = HeadlessGL::Get();
|
||||||
|
if (!gl.Usable()) {
|
||||||
|
if (RequireGpu()) {
|
||||||
|
// FAIL() is a FATAL failure but does NOT mark the test skipped,
|
||||||
|
// so a derived SetUp that guards on IsSkipped() alone would run
|
||||||
|
// straight into GL calls with no current context and SIGSEGV -
|
||||||
|
// that exact crash shipped from the first version of this guard.
|
||||||
|
// Derived fixtures must gate on Ready() (below), which is false
|
||||||
|
// on BOTH the skip path and this failure path.
|
||||||
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so an unusable harness is a failure, not a skip. "
|
||||||
|
<< "Backend " << gl.BackendName() << " could not be brought up: " << gl.SkipReason();
|
||||||
|
}
|
||||||
|
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||||
|
}
|
||||||
|
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||||
|
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
||||||
|
// unusable harness is a failure, not a silent skip" - it is the
|
||||||
|
// falsifiability switch, and CI is exactly where it belongs. But CI
|
||||||
|
// runners have no GPU, so folding "must not be llvmpipe" into the
|
||||||
|
// same switch made the CI lane unpassable by construction: the
|
||||||
|
// scenarios pin backend draw logic, which a software rasterizer
|
||||||
|
// executes just as faithfully. Landing on llvmpipe/lavapipe there is
|
||||||
|
// the intended configuration, not a misconfiguration. A vendor pin
|
||||||
|
// that must not silently degrade sets REQUIRE_HARDWARE_GPU.
|
||||||
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software "
|
||||||
|
<< "rasterizer: " << gl.RendererString();
|
||||||
|
}
|
||||||
|
// A scenario starts from a clean slate but shares the context (and so
|
||||||
|
// the renderer's memos) with every other scenario in this process -
|
||||||
|
// which is exactly the situation both shipped bugs needed.
|
||||||
|
RecordProperty("backend", gl.BackendName());
|
||||||
|
RecordProperty("renderer", gl.RendererString());
|
||||||
|
m_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ONLY gate a derived SetUp/TearDown may use: `if (!Ready()) return;`.
|
||||||
|
// True only when the base SetUp brought the context up and neither skipped
|
||||||
|
// nor failed. IsSkipped() alone is WRONG here (see the comment at FAIL()).
|
||||||
|
bool Ready() const { return m_ready; }
|
||||||
|
|
||||||
|
static HeadlessGL& Gl() { return HeadlessGL::Get(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool LooksLikeSoftwareRasterizer(const std::string& renderer) {
|
||||||
|
static const char* kNames[] = {"llvmpipe", "lavapipe", "softpipe", "SwiftShader", "swrast"};
|
||||||
|
for (const char* name : kNames) {
|
||||||
|
if (renderer.find(name) != std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool m_ready = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Main.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
|
||||||
|
//
|
||||||
|
// Entry point for the headless GPU integration scenarios.
|
||||||
|
//
|
||||||
|
// The banner lives in a gtest Environment rather than in main() on purpose:
|
||||||
|
// Environment::SetUp does not run for `--gtest_list_tests`, which is what CMake
|
||||||
|
// invokes at build time to discover the cases. Discovery therefore never brings
|
||||||
|
// up EGL, never needs a GPU and cannot hang.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "Harness/HeadlessGL.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class HarnessBanner : public ::testing::Environment {
|
||||||
|
public:
|
||||||
|
void SetUp() override {
|
||||||
|
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||||
|
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||||
|
if (gl.Usable()) {
|
||||||
|
// EGL_PLATFORM is echoed because it is the invariant this harness
|
||||||
|
// rests on: the run is headless on every machine, so a run that
|
||||||
|
// silently bound to a workstation's window system is a different
|
||||||
|
// run from CI's and must be visible as one in the log.
|
||||||
|
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
constexpr const char* surfaceKind = "AImageReader window";
|
||||||
|
#else
|
||||||
|
constexpr const char* surfaceKind = "pbuffer";
|
||||||
|
#endif
|
||||||
|
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
|
||||||
|
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||||
|
surfaceKind,
|
||||||
|
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||||
|
} else if (MGITest::RequireGpu()) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
||||||
|
gl.SkipReason().c_str());
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
" SKIPPING every scenario: %s\n"
|
||||||
|
" (set MOBILEGL_ITEST_REQUIRE_GPU=1 to make this a failure instead - a run that\n"
|
||||||
|
" skipped everything is otherwise indistinguishable from one that passed)\n",
|
||||||
|
gl.SkipReason().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override { MGITest::HeadlessGL::Get().ShutDown(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
::testing::AddGlobalTestEnvironment(new HarnessBanner());
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.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 E - asynchronous shader compilation and GL_KHR_parallel_shader_compile
|
||||||
|
// on a REAL driver.
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS ALONGSIDE THE UNIT SUITES. MG_Test/Program's async suites already
|
||||||
|
// drive the same GL entry points, but they stop at the frontend: nothing there ever
|
||||||
|
// reaches a driver, so nothing there can catch the failure this scenario is built for
|
||||||
|
// - artifacts produced on a worker thread that the BACKEND then rejects, mis-binds or
|
||||||
|
// renders differently from the ones the GL thread produced. The frontend cannot tell
|
||||||
|
// the two apart; a pixel can.
|
||||||
|
//
|
||||||
|
// The five things it pins, in order:
|
||||||
|
//
|
||||||
|
// (a) 64 heavy compiles are enqueued and polled through GL_COMPLETION_STATUS_KHR.
|
||||||
|
// At least one must be observed GL_FALSE - i.e. the query really answers while
|
||||||
|
// work is outstanding rather than silently joining. Skipped, never failed, when
|
||||||
|
// the machine drained the whole batch before the first poll: a fast box must not
|
||||||
|
// be able to turn this into a red.
|
||||||
|
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
||||||
|
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
|
||||||
|
// (c) The extension string matches the configuration. This is the half a recorded
|
||||||
|
// trace can never cover - Iris and Sodium change their submission schedule the
|
||||||
|
// moment they see the string - so it is asserted against a real backend's real
|
||||||
|
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
||||||
|
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||||
|
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||||
|
// is synchronous. That is what the extension requires of a zero count.
|
||||||
|
// (e) THE ONE THAT NEEDS A GPU: the same frame, drawn with programs compiled and
|
||||||
|
// linked asynchronously and then with programs compiled and linked inline, must
|
||||||
|
// come out byte-identical under glReadPixels. Anything the worker thread got
|
||||||
|
// wrong about the compile environment, the reflection or the SPIR-V shows up
|
||||||
|
// here as a pixel difference and nowhere else.
|
||||||
|
//
|
||||||
|
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||||
|
// so this file runs twice per ctest invocation.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#include "Config.h"
|
||||||
|
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
// GL_KHR_parallel_shader_compile. Spelled out rather than relying on the host's
|
||||||
|
// glext.h: this module is built against whatever GL headers the machine has, and an
|
||||||
|
// older one has neither token. Both are also GL_*_ARB with identical values.
|
||||||
|
#ifndef GL_MAX_SHADER_COMPILER_THREADS_KHR
|
||||||
|
#define GL_MAX_SHADER_COMPILER_THREADS_KHR 0x91B0
|
||||||
|
#endif
|
||||||
|
#ifndef GL_COMPLETION_STATUS_KHR
|
||||||
|
#define GL_COMPLETION_STATUS_KHR 0x91B1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The entry point under test, resolved by the linker straight into MobileGL_s like
|
||||||
|
// every other gl* call in this module. Declared here for the same reason as the
|
||||||
|
// tokens above.
|
||||||
|
extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using MobileGL::MG_Config::QuirkOverride;
|
||||||
|
|
||||||
|
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
||||||
|
// thing that can differ between the two compilation modes is the compilation.
|
||||||
|
constexpr const char* kVertexSource = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Asymmetric in both axes, so a mode difference that also happens to be a
|
||||||
|
// symmetry of the image cannot hide (the same reason OrientationScenario draws
|
||||||
|
// quadrants rather than stripes).
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AppendQuad(std::vector<Vertex>& out, float x0, float x1, float y0, float y1, float r, float g, float b) {
|
||||||
|
const Vertex bl{x0, y0, r, g, b};
|
||||||
|
const Vertex br{x1, y0, r, g, b};
|
||||||
|
const Vertex tr{x1, y1, r, g, b};
|
||||||
|
const Vertex tl{x0, y1, r, g, b};
|
||||||
|
out.insert(out.end(), {bl, br, tr, bl, tr, tl});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vertex> QuadrantGeometry() {
|
||||||
|
std::vector<Vertex> vertices;
|
||||||
|
vertices.reserve(24);
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f); // bottom-left: blue
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f); // bottom-right: green
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f); // top-left: red
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f); // top-right: white
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expensive enough that a compile is not instantaneous, and distinct per index so
|
||||||
|
// the source-hash memo never turns one into a no-op: without both properties the
|
||||||
|
// pool has no backlog and (a) has nothing to observe.
|
||||||
|
std::string BulkyFragmentSource(int index) {
|
||||||
|
std::string source = "#version 330 core\n";
|
||||||
|
source += "in vec3 vColor;\nout vec4 oColor;\n";
|
||||||
|
source += "uniform float uSeed" + std::to_string(index) + ";\n";
|
||||||
|
source += "void main() {\n float acc = uSeed" + std::to_string(index) + ";\n";
|
||||||
|
for (int i = 0; i < 320; ++i) {
|
||||||
|
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
||||||
|
}
|
||||||
|
source += " oColor = vec4(vColor * acc, 1.0);\n}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
||||||
|
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
||||||
|
// it in-process is what lets ONE ctest run compare the two modes against each
|
||||||
|
// other - the whole point of (e).
|
||||||
|
class AsyncModeScope {
|
||||||
|
public:
|
||||||
|
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
||||||
|
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
||||||
|
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||||
|
AsyncModeScope(const AsyncModeScope&) = delete;
|
||||||
|
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
||||||
|
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
||||||
|
class OptimisticStatusScope {
|
||||||
|
public:
|
||||||
|
explicit OptimisticStatusScope(const QuirkOverride mode)
|
||||||
|
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||||
|
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||||
|
}
|
||||||
|
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||||
|
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||||
|
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||||
|
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||||
|
class CompilerThreadScope {
|
||||||
|
public:
|
||||||
|
CompilerThreadScope() = default;
|
||||||
|
~CompilerThreadScope() {
|
||||||
|
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
||||||
|
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
||||||
|
pool.SetMaxConcurrency(pool.GetThreadCount());
|
||||||
|
}
|
||||||
|
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||||
|
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLint ShaderCompletion(GLuint shader) {
|
||||||
|
GLint status = -1;
|
||||||
|
glGetShaderiv(shader, GL_COMPLETION_STATUS_KHR, &status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint ShaderCompileStatus(GLuint shader) {
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ShaderInfoLog(GLuint shader) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
if (length <= 0) return std::string();
|
||||||
|
std::vector<char> buffer(static_cast<std::size_t>(length));
|
||||||
|
GLsizei written = 0;
|
||||||
|
glGetShaderInfoLog(shader, length, &written, buffer.data());
|
||||||
|
return std::string(buffer.data(), static_cast<std::size_t>(written));
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncCompileScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const std::vector<Vertex> vertices = QuadrantGeometry();
|
||||||
|
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));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh program every time, compiled and linked in whatever mode is in
|
||||||
|
// force. Reusing one would defeat the comparison: the second mode would just
|
||||||
|
// read the first mode's artifacts back out of the memo.
|
||||||
|
GLuint BuildProgram() {
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
EXPECT_NE(program, 0u) << error;
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image DrawFrameWith(GLuint program) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueues `count` distinct heavy compiles and returns their names WITHOUT
|
||||||
|
// reading anything back, so the pool is left with a real backlog.
|
||||||
|
std::vector<GLuint> EnqueueBacklog(int count, int seedBase) {
|
||||||
|
std::vector<GLuint> shaders;
|
||||||
|
shaders.reserve(static_cast<std::size_t>(count));
|
||||||
|
m_sources.reserve(m_sources.size() + static_cast<std::size_t>(count));
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
m_sources.push_back(BulkyFragmentSource(seedBase + i));
|
||||||
|
const char* text = m_sources.back().c_str();
|
||||||
|
const GLuint shader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(shader, 1, &text, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
shaders.push_back(shader);
|
||||||
|
}
|
||||||
|
return shaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_vbo = 0;
|
||||||
|
int m_vertexCount = 0;
|
||||||
|
// Kept alive for the whole case: glShaderSource copies, but keeping the
|
||||||
|
// strings makes a failure message able to name the source it came from.
|
||||||
|
std::vector<std::string> m_sources;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- (a) + (b) ------------------------------------------------------------
|
||||||
|
// A backlog is enqueued, polled without joining, then forced to settle and
|
||||||
|
// checked for correctness. Both halves in one case on purpose: (b) is only
|
||||||
|
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||||
|
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
// One worker, so the queue behind it is what the poll observes.
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
|
const std::vector<GLuint> shaders = EnqueueBacklog(64, 6000);
|
||||||
|
|
||||||
|
int outstanding = 0;
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
const GLint completion = ShaderCompletion(shader);
|
||||||
|
ASSERT_TRUE(completion == GL_TRUE || completion == GL_FALSE)
|
||||||
|
<< "GL_COMPLETION_STATUS_KHR returned " << completion;
|
||||||
|
if (completion == GL_FALSE) ++outstanding;
|
||||||
|
}
|
||||||
|
if (outstanding == 0) {
|
||||||
|
GTEST_SKIP() << "this machine drained 64 heavy compiles before the first poll; "
|
||||||
|
"nothing was outstanding to observe";
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) Forced join: every one of them is correct, and usable.
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
EXPECT_EQ(ShaderCompileStatus(shader), GL_TRUE) << ShaderInfoLog(shader);
|
||||||
|
EXPECT_TRUE(ShaderInfoLog(shader).empty());
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE) << "GL_COMPILE_STATUS must have joined";
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a link over one of them really produces a usable program on this driver.
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, shaders.front());
|
||||||
|
glBindAttribLocation(program, 0, "aPos");
|
||||||
|
glBindAttribLocation(program, 1, "aColor");
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
EXPECT_EQ(linked, GL_TRUE);
|
||||||
|
EXPECT_GE(glGetUniformLocation(program, "uSeed6000"), 0);
|
||||||
|
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (c) ------------------------------------------------------------------
|
||||||
|
// The extension string, read from a real backend that really brought a driver
|
||||||
|
// up. No mode forcing here: a backend builds its advertised list once, from the
|
||||||
|
// configuration in force at its first use, so the meaningful assertion is
|
||||||
|
// against the AMBIENT configuration - which is exactly what makes this case
|
||||||
|
// worth running in both of the suite's flag states.
|
||||||
|
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
|
||||||
|
|
||||||
|
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||||
|
ASSERT_NE(extensions, nullptr);
|
||||||
|
const std::string extensionString(extensions);
|
||||||
|
const bool inString = extensionString.find("GL_KHR_parallel_shader_compile") != std::string::npos;
|
||||||
|
EXPECT_EQ(inString, expected)
|
||||||
|
<< "backend " << Gl().BackendName() << " GL_EXTENSIONS = " << extensionString;
|
||||||
|
|
||||||
|
// LWJGL builds GLCapabilities from the INDEXED form on a core profile, so the
|
||||||
|
// two spellings disagreeing would be invisible to the check above and fatal
|
||||||
|
// to a real application.
|
||||||
|
GLint count = 0;
|
||||||
|
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||||
|
ASSERT_GT(count, 0);
|
||||||
|
bool inIndexed = false;
|
||||||
|
for (GLint i = 0; i < count; ++i) {
|
||||||
|
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
|
||||||
|
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") inIndexed = true;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(inIndexed, expected);
|
||||||
|
|
||||||
|
// The companion query, which an application reads right after the string.
|
||||||
|
GLint maxThreads = -1;
|
||||||
|
glGetIntegerv(GL_MAX_SHADER_COMPILER_THREADS_KHR, &maxThreads);
|
||||||
|
if (expected) {
|
||||||
|
EXPECT_GE(maxThreads, 1);
|
||||||
|
} else {
|
||||||
|
EXPECT_EQ(maxThreads, 0);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (d) ------------------------------------------------------------------
|
||||||
|
// A zero count must leave nothing in flight and keep it that way.
|
||||||
|
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
|
const std::vector<GLuint> backlog = EnqueueBacklog(48, 6200);
|
||||||
|
glMaxShaderCompilerThreadsKHR(0);
|
||||||
|
|
||||||
|
for (const GLuint shader : backlog) {
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE)
|
||||||
|
<< "glMaxShaderCompilerThreadsKHR(0) must join everything still in flight";
|
||||||
|
EXPECT_EQ(ShaderCompileStatus(shader), GL_TRUE) << ShaderInfoLog(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compilation after the zero count is synchronous too.
|
||||||
|
const std::vector<GLuint> serial = EnqueueBacklog(6, 6300);
|
||||||
|
for (const GLuint shader : serial) {
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE) << "a compile after a zero count must be synchronous";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const GLuint shader : backlog) glDeleteShader(shader);
|
||||||
|
for (const GLuint shader : serial) glDeleteShader(shader);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (e) ------------------------------------------------------------------
|
||||||
|
// The one that needs the GPU. Two programs, identical source, one built with
|
||||||
|
// compilation and linking on worker threads and one built inline; the frames
|
||||||
|
// they draw must be byte-identical.
|
||||||
|
//
|
||||||
|
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||||
|
// backend's orientation and present path live, so the comparison covers the
|
||||||
|
// whole pipeline rather than the reflection tables alone.
|
||||||
|
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
Image asyncImage;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
asyncImage = DrawFrameWith(program);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image syncImage;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(false);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
syncImage = DrawFrameWith(program);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_FALSE(asyncImage.Empty());
|
||||||
|
ASSERT_FALSE(syncImage.Empty());
|
||||||
|
// The frame is the expected one in the first place - two identically WRONG
|
||||||
|
// frames would otherwise pass.
|
||||||
|
EXPECT_EQ(asyncImage.QuadrantSignature(), "blue,green,red,white")
|
||||||
|
<< "the asynchronously compiled program did not draw the expected frame";
|
||||||
|
EXPECT_EQ(asyncImage, syncImage)
|
||||||
|
<< "asynchronous and synchronous compilation rendered different frames ("
|
||||||
|
<< asyncImage.ByteDiffCount(syncImage) << " bytes differ); backend " << Gl().BackendName();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same comparison over a batch, which is the shape a shaderpack load has:
|
||||||
|
// many programs enqueued before any of them is read back, then each one drawn.
|
||||||
|
// A per-worker state leak (glslang's thread-local pools are the obvious
|
||||||
|
// candidate) shows up here and not in the single-program case above.
|
||||||
|
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPrograms = 12;
|
||||||
|
|
||||||
|
std::vector<GLuint> programs;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
// Everything enqueued before anything is read: the only shape in which
|
||||||
|
// more than one job is in flight at a time.
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
programs.push_back(BuildProgram());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
ASSERT_NE(programs[static_cast<std::size_t>(i)], 0u) << "program " << i;
|
||||||
|
const Image image = DrawFrameWith(programs[static_cast<std::size_t>(i)]);
|
||||||
|
EXPECT_EQ(image.QuadrantSignature(), "blue,green,red,white") << "program " << i;
|
||||||
|
}
|
||||||
|
for (const GLuint program : programs) glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Iris two-phase shape end to end on a real driver, with the optimistic-status
|
||||||
|
// quirk on: phase 1 compiles each stage and reads its log then its status (both
|
||||||
|
// answered optimistically), links, detaches and deletes the shaders for every
|
||||||
|
// program with no program-level read anywhere; phase 2 then checks every link and
|
||||||
|
// draws every program. Deliberately NOT built on the harness CompileProgram(),
|
||||||
|
// whose status read would join and collapse the phase-1 overlap this exists to
|
||||||
|
// exercise. What the unit suite cannot see - worker-produced artifacts the backend
|
||||||
|
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||||
|
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPrograms = 12;
|
||||||
|
|
||||||
|
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||||
|
// a compile into a no-op) but a pure pass-through at runtime: the bulk sits in
|
||||||
|
// a branch a zero-initialised uniform never takes.
|
||||||
|
const auto fragmentSource = [](const int index) {
|
||||||
|
std::string source = "#version 330 core\nin vec3 vColor;\nout vec4 oColor;\n";
|
||||||
|
source += "uniform float uGate" + std::to_string(index) + ";\n";
|
||||||
|
source += "void main() {\n oColor = vec4(vColor, 1.0);\n";
|
||||||
|
source += " if (uGate" + std::to_string(index) + " > 1e30) {\n float acc = 1.0;\n";
|
||||||
|
for (int i = 0; i < 60; ++i) {
|
||||||
|
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
||||||
|
}
|
||||||
|
source += " oColor = vec4(acc);\n }\n}\n";
|
||||||
|
return source;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<GLuint> programs;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
m_sources.push_back(fragmentSource(i));
|
||||||
|
const char* fsText = m_sources.back().c_str();
|
||||||
|
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
(void)ShaderInfoLog(vs); // Iris's exact order: the log first...
|
||||||
|
(void)ShaderCompileStatus(vs); // ...then the status; both optimistic.
|
||||||
|
|
||||||
|
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fs, 1, &fsText, nullptr);
|
||||||
|
glCompileShader(fs);
|
||||||
|
(void)ShaderInfoLog(fs);
|
||||||
|
(void)ShaderCompileStatus(fs);
|
||||||
|
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
glBindAttribLocation(program, 0, "aPos");
|
||||||
|
glBindAttribLocation(program, 1, "aColor");
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDetachShader(program, vs);
|
||||||
|
glDetachShader(program, fs);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
programs.push_back(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
const GLuint program = programs[static_cast<std::size_t>(i)];
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
ASSERT_EQ(linked, GL_TRUE) << "program " << i;
|
||||||
|
const Image image = DrawFrameWith(program);
|
||||||
|
EXPECT_EQ(image.QuadrantSignature(), "blue,green,red,white") << "program " << i;
|
||||||
|
}
|
||||||
|
for (const GLuint program : programs) glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/BufferTextureScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A BUFFER TEXTURE IS SAMPLED FROM THE VERTEX STAGE, AND TRACKS ITS BUFFER.
|
||||||
|
//
|
||||||
|
// Buffer textures are core in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
|
||||||
|
// application may build geometry out of one without asking whether the host can. Minecraft
|
||||||
|
// 26.3 does exactly that: its cloud layer has no vertex attributes at all, only gl_VertexID
|
||||||
|
// and texelFetch on a GL_R8I buffer texture. Nothing covered that path end to end on either
|
||||||
|
// backend - the frontend unit tests stop at glTexBuffer's state, and no scenario ever drew
|
||||||
|
// with the result - which is how DirectGLES came to emit `#extension GL_EXT_texture_buffer :
|
||||||
|
// require` unconditionally, compile nothing on a host without the extension, and lose the
|
||||||
|
// whole cloud layer with no diagnostic anywhere.
|
||||||
|
//
|
||||||
|
// Two claims, in the order they can break:
|
||||||
|
// 1. a vertex-stage texelFetch on an R8I buffer texture reads the byte the application put
|
||||||
|
// in the buffer (the shape of the real workload: no attributes, index from gl_VertexID);
|
||||||
|
// 2. a later glBufferSubData is visible to the next draw WITHOUT re-specifying the texture.
|
||||||
|
// glTexBuffer attaches storage, it does not copy: the texture is a live view of the
|
||||||
|
// buffer, so a backend that only refreshes the view when the texture's own state changes
|
||||||
|
// must still show the new bytes. DirectGLES' respecify gate is keyed on the texture info
|
||||||
|
// and deliberately does not include the buffer's contents, so this is the assertion that
|
||||||
|
// says that is safe rather than merely untested.
|
||||||
|
//
|
||||||
|
// NOTE ON A HOST WITHOUT BUFFER TEXTURES: this scenario is expected to FAIL there, and that is
|
||||||
|
// the honest outcome - MobileGL keeps advertising GL_MAX_TEXTURE_BUFFER_SIZE (an OpenGL 4.x
|
||||||
|
// context may not answer 0), so there is no capability an application, or this test, could
|
||||||
|
// branch on. The driver POST's "Buffer textures" row is where that verdict is stated.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// No vertex attributes: the quad's corners come from gl_VertexID, exactly like the
|
||||||
|
// workload this exists for. The texel is fetched in the VERTEX stage - the stage where
|
||||||
|
// buffer-texture support is scarcest across ES drivers - and carried flat so every
|
||||||
|
// fragment of the quad reports the same byte and the readback is exact.
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
uniform isamplerBuffer uFaces;
|
||||||
|
flat out int vFace;
|
||||||
|
void main() {
|
||||||
|
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||||
|
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||||
|
vFace = texelFetch(uFaces, 0).r;
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// 1/255 steps survive an RGBA8 round trip exactly, so the readback byte IS the value
|
||||||
|
// the vertex shader fetched.
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
flat in int vFace;
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(float(vFace) / 255.0, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A buffer texture bound as a WRITABLE image: the shader reads one texel and writes
|
||||||
|
// another, so a single dispatch proves the read direction (which already worked) and
|
||||||
|
// the write direction (which is what this exists for) apart from each other.
|
||||||
|
constexpr const char* kImageBufferCS = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 0, rgba8) uniform imageBuffer uImage;
|
||||||
|
void main() {
|
||||||
|
vec4 read = imageLoad(uImage, 1);
|
||||||
|
imageStore(uImage, 0, vec4(0.0, 1.0, 0.0, 1.0));
|
||||||
|
imageStore(uImage, 2, read);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class BufferTextureScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
bool ComputeImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 1 && maxComputeImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int MakeComputeProgram(const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draws the full-viewport quad and returns the red byte every fragment was painted with,
|
||||||
|
// or -1 if the quad did not come out uniform (which would mean the flat varying, not the
|
||||||
|
// fetch, is what this test is measuring).
|
||||||
|
int PaintedValue(unsigned int program, int width, int height) {
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
|
||||||
|
const Image image = ReadPixels(width, height);
|
||||||
|
if (image.Empty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const int first = image.At(0, 0).r;
|
||||||
|
for (int y = 0; y < image.Height(); ++y) {
|
||||||
|
for (int x = 0; x < image.Width(); ++x) {
|
||||||
|
if (image.At(x, y).r != first) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(BufferTextureScenario, VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
// GL_R8I is the format the real workload uses. Signed, so the values stay well inside
|
||||||
|
// [0, 127] to keep the readback arithmetic honest.
|
||||||
|
constexpr signed char kInitial = 37;
|
||||||
|
constexpr signed char kUpdated = 91;
|
||||||
|
std::vector<signed char> texels(64, 0);
|
||||||
|
texels[0] = kInitial;
|
||||||
|
|
||||||
|
// The harness shares one context across every scenario in the process, so an error left
|
||||||
|
// by an earlier one would surface below as "glTexBuffer was refused".
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size()), texels.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_R8I, buffer);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_R8I) was refused";
|
||||||
|
|
||||||
|
ColorFbo target = MakeColorFbo(64, 64);
|
||||||
|
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
|
||||||
|
BindFbo(target);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glUseProgram(program);
|
||||||
|
const GLint location = glGetUniformLocation(program, "uFaces");
|
||||||
|
ASSERT_NE(location, -1) << "the buffer sampler was optimized away or never reflected";
|
||||||
|
glUniform1i(location, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kInitial))
|
||||||
|
<< "a vertex-stage texelFetch on an R8I buffer texture did not read the byte the "
|
||||||
|
"application stored (a uniform -1 here means the quad was not uniform at all)";
|
||||||
|
|
||||||
|
// The texture is a VIEW of the buffer: no glTexBuffer call follows, and none should be
|
||||||
|
// needed for the new bytes to be visible.
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferSubData(GL_TEXTURE_BUFFER, 0, 1, &kUpdated);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glBufferSubData on the texture's buffer was refused";
|
||||||
|
|
||||||
|
EXPECT_EQ(PaintedValue(program, target.width, target.height), static_cast<int>(kUpdated))
|
||||||
|
<< "the buffer texture kept showing the old contents after glBufferSubData; the "
|
||||||
|
"texture must track its buffer without being re-specified";
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
glViewport(0, 0, gl.Width(), gl.Height());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A shader may WRITE a buffer texture too, through an image unit, and the bytes it writes
|
||||||
|
// land in the backend's buffer - not in the frontend's CPU shadow, which is what MapBuffer
|
||||||
|
// and GetBufferSubData hand back. A storage-block write is flagged for exactly this reason
|
||||||
|
// and the shadow is refreshed on the next read; a buffer reached through an image unit is
|
||||||
|
// the same write through a different binding, and Espryt used to flag only the first, so
|
||||||
|
// an imageStore into a buffer texture was invisible to every CPU read that followed it -
|
||||||
|
// silently, with the correct value sitting in the driver's buffer the whole time.
|
||||||
|
//
|
||||||
|
// The read direction is asserted in the same dispatch (texel 2 is a copy of texel 1) so a
|
||||||
|
// failure here cannot be blamed on the image binding not working at all.
|
||||||
|
TEST_F(BufferTextureScenario, AnImageStoreIntoABufferTextureIsVisibleToTheCpu) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units on this host";
|
||||||
|
|
||||||
|
constexpr GLuint kRed = 0x000000ffu; // RGBA8 little-endian: r = 255
|
||||||
|
constexpr GLuint kGreen = 0xff00ff00u; // what the shader stores: (0, 1, 0, 1)
|
||||||
|
constexpr int kTexels = 16;
|
||||||
|
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
const unsigned int program = MakeComputeProgram(kImageBufferCS);
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
|
||||||
|
const std::vector<GLuint> texels(kTexels, kRed);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)), texels.data(),
|
||||||
|
GL_DYNAMIC_COPY);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glTexBuffer(GL_RGBA8) was refused";
|
||||||
|
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture on a buffer texture was refused";
|
||||||
|
|
||||||
|
glUseProgram(program);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
|
||||||
|
// Both CPU read paths, because they are two entry points onto the same refresh and a
|
||||||
|
// fix that reaches only one of them is not a fix. Everything below is EXPECT rather than
|
||||||
|
// ASSERT so that a failure still reaches the cleanup at the end: the harness shares one
|
||||||
|
// context across every scenario in the process, and a leaked buffer or image binding
|
||||||
|
// here would surface as a failure somewhere else entirely.
|
||||||
|
std::vector<GLuint> readBack(kTexels, 0u);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, static_cast<GLsizeiptr>(readBack.size() * sizeof(GLuint)),
|
||||||
|
readBack.data());
|
||||||
|
EXPECT_EQ(readBack[0], kGreen) << "glGetBufferSubData did not see the imageStore";
|
||||||
|
EXPECT_EQ(readBack[2], kRed) << "the imageLoad side of the same dispatch read the wrong texel";
|
||||||
|
|
||||||
|
const void* mapped = glMapBuffer(GL_TEXTURE_BUFFER, GL_READ_ONLY);
|
||||||
|
EXPECT_NE(mapped, nullptr) << "glMapBuffer(GL_READ_ONLY) on the texture's buffer failed";
|
||||||
|
if (mapped != nullptr) {
|
||||||
|
GLuint mappedTexel0 = 0;
|
||||||
|
std::memcpy(&mappedTexel0, mapped, sizeof(mappedTexel0));
|
||||||
|
EXPECT_EQ(mappedTexel0, kGreen) << "glMapBuffer did not see the imageStore";
|
||||||
|
glUnmapBuffer(GL_TEXTURE_BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user