mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
276
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 | ||
|
|
d7976326fa | ||
|
|
72ee7c439c | ||
|
|
cdea275227 | ||
|
|
6a02c5fea0 | ||
|
|
7db5b35a3e | ||
|
|
990e518e33 | ||
|
|
25a8f51db5 | ||
|
|
8f2b766b56 | ||
|
|
b9d8ad0421 | ||
|
|
f8069c0624 | ||
|
|
d0aae85da2 | ||
|
|
b904658b10 | ||
|
|
4b3fd11462 | ||
|
|
9be5d95440 | ||
|
|
d49d79a64b | ||
|
|
f5761ea1f3 | ||
|
|
b3f774d2c0 | ||
|
|
d524330032 | ||
|
|
f2d210b12d | ||
|
|
fd40960f70 | ||
|
|
49aab57f03 | ||
|
|
62dea3bea4 | ||
|
|
57aeeec053 | ||
|
|
9c0144d24a | ||
|
|
1e45958e01 | ||
|
|
6e6f5268fb | ||
|
|
08f98ad9ce | ||
|
|
d39a706d57 | ||
|
|
f3d52faad4 | ||
|
|
ba81ee114e | ||
|
|
c8c7b19579 | ||
|
|
0e7692251d | ||
|
|
34f09291da | ||
|
|
3b65e646e1 | ||
|
|
25b9370815 | ||
|
|
4ce808b9f2 | ||
|
|
5545d31c37 |
@@ -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'
|
||||||
|
}
|
||||||
+114
-14
@@ -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,23 +612,42 @@ 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
|
||||||
|
keep=1
|
||||||
|
fi
|
||||||
|
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
|
||||||
|
# The result artifact carries mobilegl.log, retrace.log, logcat,
|
||||||
|
# the emulator log and the actual/diff images - the only record of
|
||||||
|
# why a retrace failed. Its name ends in -<backend>-<case>, so a
|
||||||
|
# suffix match on the case name keeps both backends' results for a
|
||||||
|
# case that failed on either of them, which is what a comparison
|
||||||
|
# needs. The match is anchored at the end, so a case name that is a
|
||||||
|
# prefix of a longer one does not retain the longer one's results.
|
||||||
|
for case_name in "${!failed_cases[@]}"; do
|
||||||
|
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
|
||||||
|
keep=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((keep)); then
|
||||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||||
((retained += 1))
|
((retained += 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deleting ${artifact_name} (${artifact_id})"
|
echo "Deleting ${artifact_name} (${artifact_id})"
|
||||||
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
||||||
|
|||||||
+216
-15
@@ -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
|
||||||
@@ -177,9 +297,13 @@ jobs:
|
|||||||
uses: lukka/get-cmake@v4.3.3
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
- name: Install runtime dependencies
|
- name: Install runtime dependencies
|
||||||
|
# libegl-mesa0 is the EGL vendor library itself: DriverBench brings up a
|
||||||
|
# real GL context, and libegl1 is only glvnd's dispatch. It normally
|
||||||
|
# arrives as a Recommends of libegl1, which is too quiet a dependency for
|
||||||
|
# the one job that needs a working driver.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||||
|
|
||||||
- name: Download Linux runtime
|
- name: Download Linux runtime
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -204,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
|
||||||
@@ -212,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 }}
|
||||||
@@ -236,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
|
||||||
@@ -307,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'
|
||||||
@@ -339,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
|
||||||
@@ -347,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 }})
|
||||||
@@ -362,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')"
|
||||||
@@ -394,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
|
||||||
@@ -452,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
|
||||||
@@ -466,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
+140
-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,24 +274,41 @@ 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/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
|
||||||
|
|
||||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||||
|
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||||
|
|
||||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||||
@@ -218,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
|
||||||
@@ -241,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
|
||||||
@@ -279,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
|
||||||
@@ -302,6 +413,7 @@ endif()
|
|||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
list(APPEND SOURCE_FILES
|
list(APPEND SOURCE_FILES
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -312,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
|
||||||
@@ -321,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}")
|
||||||
@@ -338,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
|
||||||
@@ -535,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()
|
||||||
@@ -543,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
|
||||||
@@ -358,7 +361,35 @@ namespace MobileGL {
|
|||||||
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
|
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
|
||||||
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
|
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
|
||||||
// so a backend that never sets it gets the conservative answer.
|
// so a backend that never sets it gets the conservative answer.
|
||||||
Bool SupportsPerLayerFramebufferAttachment = false;
|
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
|
||||||
|
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
|
||||||
|
// set for each supported target. Deliberately per target rather than one flag: the three
|
||||||
|
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
|
||||||
|
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
|
||||||
|
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
|
||||||
|
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
|
||||||
|
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
|
||||||
|
// a backend that never sets it gets the conservative answer.
|
||||||
|
Uint32 PerLayerFramebufferAttachmentTargets = 0;
|
||||||
|
|
||||||
|
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
|
||||||
|
return (static_cast<Int>(target) >= 0 &&
|
||||||
|
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
|
||||||
|
? (1u << static_cast<Uint32>(target))
|
||||||
|
: 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
|
||||||
|
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||||
|
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||||
|
}
|
||||||
|
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||||
|
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||||
|
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||||
|
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||||
|
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||||
|
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||||
|
Bool SupportsFloat64VertexAttributes = false;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
Uint32 SubgroupSupportedStages = 0;
|
Uint32 SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1112,8 +1223,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
|
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
|
||||||
// attachment's layer passed through, so this backend really does render to the layer it was
|
// attachment's layer passed through, so this backend really does render to the layer it was
|
||||||
// given - provided the driver resolved the entry point at all.
|
// given - provided the driver resolved the entry point at all.
|
||||||
m_dynamicParameters.SupportsPerLayerFramebufferAttachment =
|
// SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
|
||||||
DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr;
|
// five upload targets to glFramebufferTextureLayer with the attachment's layer passed
|
||||||
|
// through, so this backend really does render to the layer it was given - provided the driver
|
||||||
|
// resolved the entry point at all. The cube map array is the one target that also needs
|
||||||
|
// ES-level support before it has any storage to attach.
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
|
||||||
|
if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
|
||||||
|
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||||
|
if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
||||||
|
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
||||||
|
// land on this backend regardless of what the driver underneath happens to support.
|
||||||
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -160,6 +172,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
||||||
Uint64 CurrentFrameSerial();
|
Uint64 CurrentFrameSerial();
|
||||||
Uint64 CompletedFrameSerial();
|
Uint64 CompletedFrameSerial();
|
||||||
|
// Block (up to timeoutNs) until the given frame serial provably retired on the
|
||||||
|
// GPU, using the per-frame fence ring. False when no usable fence covers the
|
||||||
|
// serial (fence-less context, foreign thread, or the slot was recycled);
|
||||||
|
// completion state is untouched in that case.
|
||||||
|
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
|
||||||
// Applies (or defers until the window surface exists) the app-requested
|
// Applies (or defers until the window surface exists) the app-requested
|
||||||
// eglSwapInterval on the native EGL surface.
|
// eglSwapInterval on the native EGL surface.
|
||||||
void SetSwapInterval(Int interval);
|
void SetSwapInterval(Int interval);
|
||||||
@@ -177,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();
|
||||||
@@ -186,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,45 +21,151 @@ 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>;
|
||||||
using BackendMap = UnorderedMap<StateObject*, BackendPtr>;
|
|
||||||
using StateRefMap = UnorderedMap<StateObject*, StateWeakPtr>;
|
// The backend twin and the weak reference that decides whether the raw key still
|
||||||
|
// names the state object the twin was built for. Both live in one entry: a
|
||||||
|
// separate liveness map answered nothing the backend probe had not already found
|
||||||
|
// and cost a second hash lookup on every Find, which the draw path runs ~10 times.
|
||||||
|
struct Entry {
|
||||||
|
BackendPtr backend;
|
||||||
|
StateWeakPtr stateRef;
|
||||||
|
};
|
||||||
|
using BackendMap = UnorderedMap<StateObject*, Entry>;
|
||||||
using iterator = typename BackendMap::iterator;
|
using iterator = typename BackendMap::iterator;
|
||||||
using const_iterator = typename BackendMap::const_iterator;
|
using const_iterator = typename BackendMap::const_iterator;
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
auto* key = stateObj.get();
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||||
auto trackedStateIt = m_stateRefs.find(key);
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
||||||
if (trackedStateIt != m_stateRefs.end() && trackedStateIt->second.expired()) {
|
EnsureProcessTeardownSentinel();
|
||||||
EraseByKey(key);
|
auto& entry = m_entries[stateObj.get()];
|
||||||
|
if (entry.stateRef.expired()) {
|
||||||
|
// The previous owner of this address is gone and the allocator handed it
|
||||||
|
// to a new object: its twin describes ids the new state object never made.
|
||||||
|
entry.backend.reset();
|
||||||
}
|
}
|
||||||
m_stateRefs[key] = stateObj;
|
entry.stateRef = stateObj;
|
||||||
return m_backendObjects[key];
|
return entry.backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator find(StateObject* stateObj) {
|
// Null when no live state object owns this key. The result points into the map, so
|
||||||
if (!IsAlive(stateObj)) {
|
// it stays valid only until the next GetOrCreate/Find/CollectGarbage on this registry.
|
||||||
EraseByKey(stateObj);
|
// Take that literally, including for Find: the map is open-addressed and erases by
|
||||||
return m_backendObjects.end();
|
// shifting the rest of the probe cluster into the hole, so an erase relocates entries
|
||||||
|
// OTHER than the erased one - and Find erases, whenever it lands on a key whose state
|
||||||
|
// object has expired. Callers that need the twin across another registry call must copy
|
||||||
|
// the BackendPtr out (or keep only the pointee, which is heap-allocated and never moves).
|
||||||
|
BackendPtr* Find(StateObject* stateObj) {
|
||||||
|
const auto entryIt = m_entries.find(stateObj);
|
||||||
|
if (entryIt == m_entries.end()) {
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
return m_backendObjects.find(stateObj);
|
if (entryIt->second.stateRef.expired()) {
|
||||||
|
m_entries.erase(entryIt);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &entryIt->second.backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
const_iterator find(StateObject* stateObj) const {
|
const BackendPtr* Find(StateObject* stateObj) const {
|
||||||
return const_cast<StateBackendObjectRegistry*>(this)->find(stateObj);
|
return const_cast<StateBackendObjectRegistry*>(this)->Find(stateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator begin() { return m_backendObjects.begin(); }
|
iterator begin() { return m_entries.begin(); }
|
||||||
const_iterator begin() const { return m_backendObjects.begin(); }
|
const_iterator begin() const { return m_entries.begin(); }
|
||||||
iterator end() { return m_backendObjects.end(); }
|
iterator end() { return m_entries.end(); }
|
||||||
const_iterator end() const { return m_backendObjects.end(); }
|
const_iterator end() const { return m_entries.end(); }
|
||||||
|
|
||||||
void CollectGarbageIfNeeded() {
|
void CollectGarbageIfNeeded() {
|
||||||
++m_gcTick;
|
++m_gcTick;
|
||||||
@@ -73,19 +179,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void CollectGarbageNow() { CollectGarbage(); }
|
void CollectGarbageNow() { CollectGarbage(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool IsAlive(StateObject* stateObj) const {
|
|
||||||
const auto trackedStateIt = m_stateRefs.find(stateObj);
|
|
||||||
if (trackedStateIt == m_stateRefs.end()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !trackedStateIt->second.expired();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EraseByKey(StateObject* stateObj) {
|
|
||||||
m_stateRefs.erase(stateObj);
|
|
||||||
m_backendObjects.erase(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectGarbage() {
|
void CollectGarbage() {
|
||||||
if (m_isCollecting) {
|
if (m_isCollecting) {
|
||||||
return;
|
return;
|
||||||
@@ -94,16 +187,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_isCollecting = true;
|
m_isCollecting = true;
|
||||||
|
|
||||||
Vector<StateObject*> staleKeys;
|
Vector<StateObject*> staleKeys;
|
||||||
staleKeys.reserve(m_stateRefs.size());
|
staleKeys.reserve(m_entries.size());
|
||||||
for (const auto& [stateKey, stateWeakRef] : m_stateRefs) {
|
for (const auto& [stateKey, entry] : m_entries) {
|
||||||
if (stateWeakRef.expired()) {
|
if (entry.stateRef.expired()) {
|
||||||
staleKeys.push_back(stateKey);
|
staleKeys.push_back(stateKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto* stateKey : staleKeys) {
|
for (auto* stateKey : staleKeys) {
|
||||||
m_stateRefs.erase(stateKey);
|
m_entries.erase(stateKey);
|
||||||
m_backendObjects.erase(stateKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isCollecting = false;
|
m_isCollecting = false;
|
||||||
@@ -111,8 +203,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr Uint32 kGCInterval = 1024;
|
static constexpr Uint32 kGCInterval = 1024;
|
||||||
StateRefMap m_stateRefs;
|
BackendMap m_entries;
|
||||||
BackendMap m_backendObjects;
|
|
||||||
Uint32 m_gcTick = 0;
|
Uint32 m_gcTick = 0;
|
||||||
Bool m_isCollecting = false;
|
Bool m_isCollecting = false;
|
||||||
};
|
};
|
||||||
@@ -120,6 +211,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
namespace BufferImpl {
|
namespace BufferImpl {
|
||||||
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
||||||
|
|
||||||
|
// --- Buffer-mutation epoch -------------------------------------------------
|
||||||
|
// Manager-wide monotonic counter: it moves whenever ANY buffer resource may
|
||||||
|
// have gone from draw-clean to dirty. Draw-path memos read it once per pass
|
||||||
|
// (CurrentBufferMutationEpoch, acquire), re-run their IsBufferDrawClean
|
||||||
|
// probes only when it moved, and stamp the PRE-pass value after a pass in
|
||||||
|
// which every probe came up clean - so a concurrent bump lands strictly
|
||||||
|
// after the stamped value and forces a re-probe on the next pass no matter
|
||||||
|
// how the probe interleaved with the mutation. Conservative-correct: a bump
|
||||||
|
// never skips work, it only re-runs the probes once.
|
||||||
|
//
|
||||||
|
// Every clean->dirty transition path bumps it (BumpBufferMutationEpoch,
|
||||||
|
// release, AFTER the mutation lands so an acquire reader that still sees
|
||||||
|
// the old epoch cannot have missed the mutation):
|
||||||
|
// * the frontend BufferBackendOps table - Respecify, SubData,
|
||||||
|
// FlushMappedRange, AcquirePersistentMap, ReadbackFromGpu, OnDestroy -
|
||||||
|
// which every frontend change-serial bump and every pending-range
|
||||||
|
// queueing reaches while ops are registered (upload, orphan/respecify,
|
||||||
|
// map flush/unmap writeback, persistent-map adoption, delete/pooling);
|
||||||
|
// * backend-initiated shadow writebacks that bump the frontend change
|
||||||
|
// serial without an op: transform-feedback capture readback
|
||||||
|
// (XfbImpl::ReadbackCapturedRanges and the scatter path) and every
|
||||||
|
// pack-PBO WritebackFromBackend site (glReadPixels/glGetTexImage);
|
||||||
|
// * RegisterBufferBackendOps/UnregisterBufferBackendOps - while ops are
|
||||||
|
// unregistered, frontend writes advance serials silently, so both edges
|
||||||
|
// of that window re-open every memo;
|
||||||
|
// * OnBackendContextDestroyed - the buffer context generation moved, so
|
||||||
|
// every previously clean resource is invalid.
|
||||||
|
// NOT bumped (cleanliness provably unchanged): MarkGpuWritten (the backend
|
||||||
|
// copy is authoritative; IsBufferDrawClean does not consult it),
|
||||||
|
// NotifyContentWrite on a GPU-resident buffer (persistent-mapped resources
|
||||||
|
// are clean by construction), and EnsureBufferResource itself (it only
|
||||||
|
// repairs toward clean). A non-persistent map (draws on it are GL errors
|
||||||
|
// the frontend rejects) sets IsMapped without an op; persistent maps reach
|
||||||
|
// AcquirePersistentMap or (FLUSH_EXPLICIT) publish only via FlushMappedRange.
|
||||||
|
Uint64 CurrentBufferMutationEpoch();
|
||||||
|
void BumpBufferMutationEpoch();
|
||||||
|
|
||||||
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
|
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
|
||||||
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
|
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
|
||||||
// draw-time "sync" reduces to ensuring the storage exists.
|
// draw-time "sync" reduces to ensuring the storage exists.
|
||||||
@@ -145,6 +273,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool pendingRespecify = false;
|
Bool pendingRespecify = false;
|
||||||
VecRange1D pendingRanges;
|
VecRange1D pendingRanges;
|
||||||
std::mutex pendingMutex;
|
std::mutex pendingMutex;
|
||||||
|
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||||
|
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||||
|
// at 1). Written only on the draw thread; per-draw resource consumers
|
||||||
|
// (the UBO binding walk) skip the probe while their pre-pass epoch read
|
||||||
|
// matches, exactly like the per-VAO memo stamps.
|
||||||
|
Uint64 drawCleanEpoch = 0;
|
||||||
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
||||||
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
||||||
// (and the frontend PipeResource) write into directly. While set, draw-time
|
// (and the frontend PipeResource) write into directly. While set, draw-time
|
||||||
@@ -152,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
|
||||||
@@ -170,6 +312,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||||
// Existing resource or nullptr; performs no GL calls.
|
// Existing resource or nullptr; performs no GL calls.
|
||||||
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
||||||
|
// True when EnsureBufferResource(frontend) would provably fall straight through
|
||||||
|
// every branch and do no work — i.e. `resource` is still the frontend's own
|
||||||
|
// resource, its id belongs to the live ES context, and either it is the
|
||||||
|
// zero-copy coherent persistent store (draw-time sync is a no-op by design) or
|
||||||
|
// the storage is initialized at the right size with no pending ops and a synced
|
||||||
|
// change serial while the buffer is not mapped (an active map may owe a
|
||||||
|
// per-draw persistent-range push, so it always takes the full path).
|
||||||
|
// `frontend` must be non-null and alive; the caller guarantees that by holding
|
||||||
|
// (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO
|
||||||
|
// resolved-buffers memo to skip EnsureBufferResource on clean static buffers.
|
||||||
|
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource);
|
||||||
|
|
||||||
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
||||||
// thread without a current ES context). Called from draw-time sync.
|
// thread without a current ES context). Called from draw-time sync.
|
||||||
@@ -193,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/
|
||||||
@@ -255,17 +416,118 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
|
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||||
|
// distinct enabled-attribute buffers (deduped) and the index buffer, resolved
|
||||||
|
// to their backend resources once. Valid while the VAO's config version is
|
||||||
|
// unchanged — every attach/enable/disable/format mutation bumps it (the same
|
||||||
|
// invariant SyncToBackend's gate already leans on), and the VAO's attribute
|
||||||
|
// SharedPtrs pin each memoed frontend buffer for exactly that long, so the raw
|
||||||
|
// pointers cannot dangle on a hit. Per-buffer cleanliness is NOT memoed here:
|
||||||
|
// each hit re-checks IsBufferDrawClean (resource identity, context generation,
|
||||||
|
// pending ops, change serial) and falls back to EnsureBufferResource for just
|
||||||
|
// the dirty entries via their attribute index. The IBO entry is keyed on the
|
||||||
|
// slot's bound-object identity instead (its slot version is a wrapping Uint16
|
||||||
|
// and is not covered by the config version).
|
||||||
|
struct ResolvedDrawBuffers {
|
||||||
|
struct Entry {
|
||||||
|
MG_State::GLState::BufferObject* frontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* resource = nullptr;
|
||||||
|
Uint8 attribIndex = 0;
|
||||||
|
};
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint count = 0;
|
||||||
|
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
|
||||||
|
MG_State::GLState::BufferObject* iboFrontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* iboResource = nullptr;
|
||||||
|
// Buffer-mutation epoch (BufferImpl::CurrentBufferMutationEpoch) at which
|
||||||
|
// the LAST probe pass found every entry / the IBO clean; 0 = not stamped
|
||||||
|
// (epochs start at 1). While a stamp matches the pre-pass epoch read, the
|
||||||
|
// probes are skipped outright: any path that can dirty ANY buffer bumps
|
||||||
|
// the epoch (the exhaustive site list lives at the epoch declaration).
|
||||||
|
// The IBO stamp is only trusted together with the bound-object identity
|
||||||
|
// compare - the VAO's index slot can rebind with no epoch or config move.
|
||||||
|
Uint64 vboCleanEpoch = 0;
|
||||||
|
Uint64 iboCleanEpoch = 0;
|
||||||
|
};
|
||||||
|
ResolvedDrawBuffers& GetResolvedDrawBuffersMemo() { return m_resolvedDrawBuffers; }
|
||||||
|
|
||||||
|
// Memo for SyncCurrentVertexAttributeValues: which of a program's ACTIVE
|
||||||
|
// attribute locations lack an enabled array in this VAO (those read the
|
||||||
|
// context's current generic value instead of a buffer). Keyed on the VAO
|
||||||
|
// config version (enable/disable bumps it) and the program's active-location
|
||||||
|
// mask. Hosted per twin — the former function-static single entry missed on
|
||||||
|
// every draw once the app cycled VAOs, re-reading the cold attribute slots.
|
||||||
|
struct PendingAttribValueMask {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint32 activeMask = 0;
|
||||||
|
Uint32 pendingMask = 0;
|
||||||
|
};
|
||||||
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
|
PendingAttribValueMask m_pendingAttribValueMask;
|
||||||
Uint m_backendVAOId = 0;
|
Uint m_backendVAOId = 0;
|
||||||
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
|
||||||
|
// 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
|
||||||
|
// compare in SyncToBackend would come up clean. The index-buffer slot has its own
|
||||||
|
// version and is NOT covered. The Bool (not a sentinel value) marks "never synced".
|
||||||
|
Bool m_hasSyncedConfigVersion = false;
|
||||||
|
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>
|
||||||
g_backendVertexArrayObjects;
|
g_backendVertexArrayObjects;
|
||||||
|
|
||||||
|
// Shadowed glBindVertexArray: every backend VAO bind goes through here so a
|
||||||
|
// draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's
|
||||||
|
// re-bind) reaches the driver once. Invalidate whenever the ES context is
|
||||||
|
// replaced - ids restart and the resting binding is 0 again.
|
||||||
|
void BindBackendVAOId(Uint id);
|
||||||
|
void InvalidateVAOBindingCache();
|
||||||
|
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||||
|
void NoteVAOIdDeleted(Uint id);
|
||||||
|
|
||||||
|
// baseInstance emulation for drivers without GL_EXT_base_instance. GL fetches an
|
||||||
|
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
|
||||||
|
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
|
||||||
|
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
|
||||||
|
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
|
||||||
|
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
|
||||||
|
void SetPendingFetchBaseInstance(Uint32 baseInstance);
|
||||||
|
Uint32 GetPendingFetchBaseInstance();
|
||||||
|
|
||||||
|
class ScopedFetchBaseInstance {
|
||||||
|
public:
|
||||||
|
explicit ScopedFetchBaseInstance(Uint32 baseInstance) { SetPendingFetchBaseInstance(baseInstance); }
|
||||||
|
~ScopedFetchBaseInstance() { SetPendingFetchBaseInstance(0); }
|
||||||
|
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
|
||||||
|
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
|
||||||
|
};
|
||||||
} // namespace VertexArrayImpl
|
} // namespace VertexArrayImpl
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
@@ -327,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;
|
||||||
@@ -364,6 +645,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||||
Uint GetBackendTextureId() const;
|
Uint GetBackendTextureId() const;
|
||||||
|
|
||||||
|
// Aggregate first-level clean gate for the per-draw trio
|
||||||
|
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
||||||
|
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
||||||
|
// (params version == synced params version; builtin-sampler version ==
|
||||||
|
// synced sampler version; and SyncMipmapsToBackend's cheap gate - stamped
|
||||||
|
// trio + content version + Mipmap storage). True means each of the three
|
||||||
|
// would provably return without work, so the caller may skip the calls;
|
||||||
|
// false only falls through to the three calls, whose own gates re-decide
|
||||||
|
// individually - this gate must never be MORE permissive than they are.
|
||||||
|
// `contextId`/`samplingGeneration` are the frontend context's current
|
||||||
|
// values, hoisted by the caller so a per-draw list walk reads them once
|
||||||
|
// instead of per texture. `t` must be the live frontend texture.
|
||||||
|
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||||
|
Uint64 samplingGeneration) const {
|
||||||
|
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||||
|
m_syncedShapeGeneration != samplingGeneration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Uint16 paramsVersion = t->GetTextureParamsVersion();
|
||||||
|
if (m_syncedShapeParamsVersion != paramsVersion || m_syncedTextureParamsVersion != paramsVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_syncedContentVersion == 0 || m_syncedContentVersion != t->GetContentVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& samplerObject = t->GetSamplerObject();
|
||||||
|
if (!samplerObject || m_syncedSamplerVersion != samplerObject->GetVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return t->GetStorageType() == TextureStorageType::Mipmap;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecreateBackendTexture();
|
void RecreateBackendTexture();
|
||||||
|
|
||||||
@@ -374,14 +687,48 @@ 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
|
||||||
|
// clean probe compares this before rebuilding shape info and scanning
|
||||||
|
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
||||||
|
Uint64 m_syncedContentVersion = 0;
|
||||||
|
// First-level clean gate for SyncMipmapsToBackend, checked before even the
|
||||||
|
// IsComplete()/shape-probe walk. Valid only as a trio with the content and
|
||||||
|
// texture-params versions: the context's sampling-resolution generation moves on
|
||||||
|
// EVERY texture-shape mutation (BumpShapeVersion is the only writer of shape and
|
||||||
|
// unconditionally bumps it), the content version on every CPU pixel mutation, and
|
||||||
|
// the params version covers SetSamples/SetFixedSampleLocations, which bump neither
|
||||||
|
// of the other two but feed the shape probe. The context id pins the generation to
|
||||||
|
// the context that produced it - generations restart at 0 with a new context, and a
|
||||||
|
// texture is owned by exactly one context (share groups are not implemented), so a
|
||||||
|
// mutation can never happen under a context this key does not name. 0 = never
|
||||||
|
// stamped (real context ids start at 1). Backend-side invalidation rides on
|
||||||
|
// m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it.
|
||||||
|
Uint64 m_syncedShapeContextId = 0;
|
||||||
|
Uint64 m_syncedShapeGeneration = 0;
|
||||||
|
Uint16 m_syncedShapeParamsVersion = 0;
|
||||||
SamplerParameters m_cacheSamplerParameters;
|
SamplerParameters m_cacheSamplerParameters;
|
||||||
UintVec2 m_cacheLodRange = {0, 1000};
|
UintVec2 m_cacheLodRange = {0, 1000};
|
||||||
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);
|
||||||
@@ -391,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
|
||||||
@@ -418,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
|
||||||
@@ -433,11 +790,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
this array could be provided as data directly to ES `glDrawBuffers` function
|
this array could be provided as data directly to ES `glDrawBuffers` function
|
||||||
*/
|
*/
|
||||||
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||||
|
|
||||||
|
static constexpr Uint MAX_COLOR_ATTACHMENT_SLOTS =
|
||||||
|
static_cast<Uint>(FramebufferAttachmentType::Color31) -
|
||||||
|
static_cast<Uint>(FramebufferAttachmentType::Color0) + 1;
|
||||||
|
/* Where each frontend GL_COLOR_ATTACHMENTn image physically lives in the backend ES
|
||||||
|
framebuffer, as a GL_COLOR_ATTACHMENTm enum. ES only accepts glDrawBuffers bufs[s] ==
|
||||||
|
GL_COLOR_ATTACHMENTs, so a GL draw-buffer slot s naming attachment a forces a's image
|
||||||
|
under backend slot s. This table is the single owner of that decision and is kept a
|
||||||
|
PERMUTATION of the backend colour slots: every other attachment keeps its identity
|
||||||
|
slot when that slot survived, and is parked on the lowest free slot when it did not.
|
||||||
|
Deriving the point per-query from the draw-buffer array instead handed the identity
|
||||||
|
point to any attachment that was not a draw buffer - i.e. exactly the point a
|
||||||
|
relocated draw buffer had just taken over. The permutation is only true of the
|
||||||
|
PHYSICAL framebuffer because the attachment loop detaches a point whose frontend
|
||||||
|
owner is empty; do not remove that detach. */
|
||||||
|
GLenum m_backendColorSlots[MAX_COLOR_ATTACHMENT_SLOTS] = {GL_NONE};
|
||||||
|
/* Rebuild m_backendColorSlots from the frontend draw-buffer array. Returns true when any
|
||||||
|
attachment moved, i.e. when the physical attachments and the memoised read buffer have
|
||||||
|
to be re-applied. */
|
||||||
|
Bool RecomputeBackendColorSlots(
|
||||||
|
const MG_State::GLState::FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers);
|
||||||
|
|
||||||
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
||||||
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
||||||
|
|
||||||
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>
|
||||||
@@ -449,14 +833,97 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// has to apply the clamp itself.
|
// has to apply the clamp itself.
|
||||||
Bool IsFixedPointFallbackReadAttachment();
|
Bool IsFixedPointFallbackReadAttachment();
|
||||||
|
|
||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
// 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)
|
||||||
|
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
||||||
|
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
||||||
|
// only meaningful together - see SyncCurrentFBO.
|
||||||
|
//
|
||||||
|
// The binding slot's own version, which changes whenever a different object is bound
|
||||||
|
// to this target. Distinguishes a rebind from an in-place edit, and keeps the raw
|
||||||
|
// pointer below from matching an address the allocator recycled for a new FBO.
|
||||||
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions;
|
||||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
||||||
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
||||||
// must re-sync it even when the binding-slot version has not moved.
|
// must re-sync it even when the binding-slot version has not moved.
|
||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||||
|
// Which object was synced. Raw and never dereferenced: only compared for identity.
|
||||||
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
|
||||||
@@ -467,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
|
||||||
@@ -549,6 +1020,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void InvalidatePackStateCache();
|
void InvalidatePackStateCache();
|
||||||
} // namespace PixelStoreImpl
|
} // namespace PixelStoreImpl
|
||||||
|
|
||||||
|
namespace SamplerImpl {
|
||||||
|
class BackendSamplerObject; // for PrgramImpl's sampler-pass memo rows below
|
||||||
|
}
|
||||||
|
|
||||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||||
// assigned through glUniform1i.
|
// assigned through glUniform1i.
|
||||||
@@ -595,6 +1070,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Float lastAssignedLodBias = 0.0f;
|
Float lastAssignedLodBias = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Memo of the whole per-draw sampler-uniform pass (glUniform1i unit
|
||||||
|
// assignments, lod-bias uniform, raw-depth-fetch substitution and the
|
||||||
|
// per-unit sampler-object binds) in BindCurrentProgramWithResources.
|
||||||
|
// The pass is a pure function of the keys below, and its only driver-side
|
||||||
|
// effect is the sampler binding of each sampled unit, so replaying it as
|
||||||
|
// "do nothing" additionally requires those bindings to still be on the
|
||||||
|
// driver - the per-entry row compare against g_boundSamplersCache (the
|
||||||
|
// shadow every sampler bind in this backend already routes through).
|
||||||
|
//
|
||||||
|
// Invalidation enumeration:
|
||||||
|
// * sampler-uniform unit assignment (glUniform1i) and uniform-block
|
||||||
|
// binding edits -> frontend backendStateVersion;
|
||||||
|
// * any texture/sampler bind moving on any unit (incl. the high-water
|
||||||
|
// mark moving) -> unitBindingsEpoch;
|
||||||
|
// * any sampler parameter (incl. lod bias, compare mode) or texture
|
||||||
|
// shape/format change -> samplingGeneration;
|
||||||
|
// * another frontend context -> contextId (never-reused id);
|
||||||
|
// * ES context recreation -> textureContextGeneration;
|
||||||
|
// * relink / backend program rebuild -> SyncToBackend resets `valid`
|
||||||
|
// (it rebuilds m_samplerUniformBindings, whose lastAssignedUnit /
|
||||||
|
// lastAssignedLodBias dedup state this memo leans on);
|
||||||
|
// * any other writer moving a sampled unit's sampler binding
|
||||||
|
// (BindCurrentUnitSamplers on a unit-sampler change, scratch binds)
|
||||||
|
// -> the row snapshot compare.
|
||||||
|
struct SamplerPassMemo {
|
||||||
|
static constexpr SizeT kMaxEntries = 16;
|
||||||
|
Bool valid = false;
|
||||||
|
Uint8 count = 0;
|
||||||
|
Uint64 contextId = 0;
|
||||||
|
Uint64 unitBindingsEpoch = 0;
|
||||||
|
Uint64 samplingGeneration = 0;
|
||||||
|
Uint32 backendStateVersion = 0;
|
||||||
|
Uint textureContextGeneration = 0;
|
||||||
|
Array<Uint8, kMaxEntries> units{};
|
||||||
|
Array<SamplerImpl::BackendSamplerObject*, kMaxEntries> rows{};
|
||||||
|
};
|
||||||
|
|
||||||
BackendProgramObjectImpl();
|
BackendProgramObjectImpl();
|
||||||
~BackendProgramObjectImpl();
|
~BackendProgramObjectImpl();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
@@ -602,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
|
||||||
@@ -612,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; }
|
||||||
@@ -623,17 +1147,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// reflected size when the transpiled block pads differently).
|
// reflected size when the transpiled block pads differently).
|
||||||
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
||||||
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
||||||
|
SamplerPassMemo& GetSamplerPassMemo() { return m_samplerPassMemo; }
|
||||||
// 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;
|
||||||
@@ -641,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;
|
||||||
|
|
||||||
@@ -651,6 +1212,14 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Uint32 g_snormFallbackClampOutputMask;
|
extern Uint32 g_snormFallbackClampOutputMask;
|
||||||
@@ -665,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;
|
||||||
@@ -694,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,9 +203,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
namespace {
|
||||||
const SizeT targetIndex =
|
Bool BackendFormatAddsAlpha(TextureInternalFormat internalFormat, SizeT targetIndex) {
|
||||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
|
||||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -188,11 +212,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
const Flags<PixelFormatNormalizeOptionBit> options =
|
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
|
||||||
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||||
GetRenderTargetNormalizeOptions(targetIndex));
|
|
||||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||||
|
const SizeT targetIndex =
|
||||||
|
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||||
|
return BackendFormatAddsAlpha(internalFormat, targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||||
|
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
|
}
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
String ProcessOutColorLocations(const String& glslCode) {
|
String ProcessOutColorLocations(const String& glslCode) {
|
||||||
@@ -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,26 +1596,18 @@ 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);
|
|
||||||
if (dstPixelBytes == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PackedReadbackLayout packedLayout{};
|
|
||||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
|
||||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
|
||||||
|
|
||||||
const auto& pixelPackBufferObject =
|
const auto& pixelPackBufferObject =
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||||
|
|
||||||
@@ -1071,29 +1634,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
|
||||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
|
||||||
Vector<Uint8> convertedRow(dstRowBytes);
|
Vector<Uint8> convertedRow(dstRowBytes);
|
||||||
|
|
||||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
for (GLsizei 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,7 +1661,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||||
|
// the buffer draw-clean memos (once for the whole row loop).
|
||||||
|
BufferImpl::BumpBufferMutationEpoch();
|
||||||
|
}
|
||||||
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);
|
||||||
@@ -810,6 +896,50 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||||
|
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
|
||||||
|
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
|
||||||
|
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
|
||||||
|
// layered targets are declared separately as their own machinery lands.
|
||||||
|
{
|
||||||
|
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||||
|
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
|
||||||
|
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
|
||||||
|
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
|
||||||
|
// image can name. Optimistic: a format that refuses the flag is caught at image creation
|
||||||
|
// and declines the slice view there, which the clear path handles as a soft miss.
|
||||||
|
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
|
||||||
|
}
|
||||||
|
if (m_vulkanCaps.SupportsImageCubeArray) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
@@ -818,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
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <bit>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
@@ -68,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};
|
||||||
@@ -155,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;
|
||||||
@@ -231,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,
|
||||||
@@ -255,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() {
|
||||||
@@ -394,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) {
|
||||||
@@ -486,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;
|
||||||
}
|
}
|
||||||
@@ -534,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,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;
|
||||||
@@ -1233,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");
|
||||||
@@ -1392,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) {
|
||||||
@@ -1444,40 +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;
|
|
||||||
|
|
||||||
// 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] * MG_Util::GetGLTypeSize(type),
|
|
||||||
payload.indexBufferView.indexByteSize);
|
|
||||||
|
|
||||||
auto& param = params[i];
|
|
||||||
|
|
||||||
param.indexCount = count[i];
|
|
||||||
param.instanceCount = 1;
|
|
||||||
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) / MG_Util::GetGLTypeSize(type);
|
|
||||||
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,9 +206,12 @@ 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)));
|
||||||
|
XXHASH_VERIFY(
|
||||||
|
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||||
@@ -250,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;
|
||||||
@@ -387,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;
|
||||||
@@ -397,6 +421,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
||||||
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
||||||
raster.lineWidth = 1.0f;
|
raster.lineWidth = 1.0f;
|
||||||
|
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
|
||||||
|
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
|
||||||
|
// selector already collapses to FIRST in exactly that case - so a device without the
|
||||||
|
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
|
||||||
|
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
|
||||||
|
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
|
||||||
|
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||||
|
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
|
||||||
|
provokingVertexState.pNext = raster.pNext;
|
||||||
|
raster.pNext = &provokingVertexState;
|
||||||
|
}
|
||||||
|
|
||||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||||
@@ -445,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 =
|
||||||
@@ -464,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),
|
||||||
@@ -494,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,9 +42,22 @@ 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;
|
||||||
|
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
|
||||||
|
// which vertex a flat varying takes AND the vertex order transform feedback records for
|
||||||
|
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
|
||||||
|
// Vulkan's own convention, which is what a device without the extension gets.
|
||||||
|
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||||
Bool depthTestEnable = false;
|
Bool depthTestEnable = false;
|
||||||
Bool depthWriteEnable = false;
|
Bool depthWriteEnable = false;
|
||||||
Bool depthBiasEnable = false;
|
Bool depthBiasEnable = false;
|
||||||
@@ -56,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,25 +59,69 @@ 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
|
||||||
|
// 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was
|
||||||
|
// scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes
|
||||||
|
// pDynamicOffsets in binding order and the writer pushes them in iteration order, so an
|
||||||
|
// unordered list would silently mis-pair dynamic offsets with their uniform blocks.
|
||||||
|
Vector<Uint32> activeBindings;
|
||||||
Vector<Uint32> dynamicBindings;
|
Vector<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.
|
||||||
@@ -79,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;
|
||||||
@@ -86,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{};
|
||||||
@@ -98,9 +167,37 @@ 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).
|
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||||
Uint64 lastUsedFrame = 0;
|
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||||
|
mutable Uint64 lastUsedFrame = 0;
|
||||||
|
|
||||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -111,9 +208,19 @@ 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);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
@@ -128,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;
|
||||||
@@ -137,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;
|
||||||
@@ -149,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 {
|
||||||
@@ -159,9 +277,12 @@ 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);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
@@ -176,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;
|
||||||
@@ -185,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;
|
||||||
@@ -197,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;
|
||||||
}
|
}
|
||||||
@@ -224,6 +356,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
modules.clear();
|
modules.clear();
|
||||||
stages.clear();
|
stages.clear();
|
||||||
|
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,21 +372,72 @@ 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
|
||||||
|
// an open-addressing map holding entries by value, so both moves existing entries.
|
||||||
|
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||||
|
// this is unchanged; on a bump it must re-run GetOrCreateProgram.
|
||||||
|
Uint64 GetCacheStructureEpoch() const { return m_cacheStructureEpoch; }
|
||||||
|
// A memoised entry pointer bypasses GetOrCreateProgram, whose per-lookup stamp is
|
||||||
|
// what keeps an in-use entry out of OnFrameBoundary's idle sweep - so such a
|
||||||
|
// caller must re-stamp the entry itself, at least once per frame boundary.
|
||||||
|
void StampProgramUse(const VkProgramObject& entry) const { entry.lastUsedFrame = m_frameCounter; }
|
||||||
|
|
||||||
// Observer may be null (no notifications). Not owned.
|
// Observer may be null (no notifications). Not owned.
|
||||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||||
@@ -272,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 {
|
||||||
@@ -285,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;
|
||||||
@@ -301,10 +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.
|
||||||
|
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,18 +64,52 @@ 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 DESCRIPTOR (post fallback substitution,
|
||||||
|
// in binding order, and within a binding in array-element order): the resolved texture
|
||||||
|
// and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
|
||||||
|
// at the same heap address can only MISS a comparison, never false-hit it (same ABA
|
||||||
|
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
|
||||||
|
// element granularity is required, or swapping the textures of two elements of the same
|
||||||
|
// array would leave the record list identical and the fast path would keep a stale set.
|
||||||
|
struct SampledBindingRecord {
|
||||||
|
Uint64 textureLifetimeId = 0;
|
||||||
|
Uint64 samplerLifetimeId = 0;
|
||||||
|
};
|
||||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||||
|
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||||
|
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||||
|
// walk and reports whether every visited binding still resolves to the recorded
|
||||||
|
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||||
|
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||||
|
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||||
|
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||||
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
|
||||||
|
// every input of every combined-image-sampler resolution is unchanged since the
|
||||||
|
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||||
|
// sum, sampling-resolution generation (sampler params + texture shape), image
|
||||||
|
// epochs AND per-resource layout values - so the per-binding cached
|
||||||
|
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
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,
|
||||||
|
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
|
||||||
@@ -87,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
|
||||||
@@ -114,28 +160,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||||
|
// Shared per-binding resolution for CollectSampledTextures and
|
||||||
|
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||||
|
// texture after the fallback substitution (may still be null when no fallback
|
||||||
|
// exists), effective sampler = unit override else the texture's own sampler.
|
||||||
|
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||||
|
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||||
|
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||||
|
MG_State::GLState::ITextureObject*& outTexture,
|
||||||
|
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
// 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
|
||||||
|
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||||
|
// obligations the caller carries). The cache is keyed by binding alone, so it is
|
||||||
|
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
|
||||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const 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) const;
|
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||||
|
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 {
|
||||||
@@ -149,8 +227,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 arrayElement, UboBindResult& out) const;
|
Uint32 arrayElement, UboBindResult& out) const;
|
||||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
// Shared resolution of one dynamic-UBO binding element into the
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
|
||||||
|
// bind, global-slice reuse, or transient upload. Used by the full walk
|
||||||
|
// and by the dynamic-offset-only rebind (see FastRebindMemo).
|
||||||
|
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
|
||||||
|
VkDeviceSize& outRange, Uint32& outDynamicOffset);
|
||||||
|
// The vkCmdBindDescriptorSets tail shared by the full walk and the
|
||||||
|
// dynamic-offset-only rebind: skips the driver call when this exact
|
||||||
|
// binding is already live on the command buffer (see the bind-dedup
|
||||||
|
// shadow below), otherwise binds and refreshes the shadow.
|
||||||
|
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||||
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||||
|
const Vector<Uint32>& dynamicOffsets);
|
||||||
|
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||||
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||||
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,
|
||||||
@@ -179,14 +272,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<VkBufferView> m_texelBufferViewsScratch;
|
Vector<VkBufferView> m_texelBufferViewsScratch;
|
||||||
Vector<Uint32> m_dynamicOffsetsScratch;
|
Vector<Uint32> m_dynamicOffsetsScratch;
|
||||||
|
|
||||||
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
|
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
|
||||||
// When a draw's resolved descriptor content is byte-identical to the previous
|
// When a draw's resolved descriptor content is byte-identical to one memoized
|
||||||
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
|
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
|
||||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
|
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
|
||||||
// frame in BeginFrame because the frame's descriptor sets are recycled there.
|
// entries with round-robin replacement rather than one: draws alternating
|
||||||
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
|
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
|
||||||
Uint64 m_lastDescriptorSignature = 0;
|
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
|
||||||
Bool m_hasLastDescriptor = false;
|
// because the frame's descriptor sets are recycled there.
|
||||||
|
struct DescriptorReuseEntry {
|
||||||
|
Uint64 signature = 0;
|
||||||
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
|
Bool valid = false;
|
||||||
|
};
|
||||||
|
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
|
||||||
|
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||||
|
Uint32 m_descriptorReuseMemoNext = 0;
|
||||||
|
|
||||||
|
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
|
||||||
|
// descriptor set selected by the last cacheable full walk of a program
|
||||||
|
// whose active bindings are exactly one dynamic UBO (single descriptor)
|
||||||
|
// plus combined-image samplers. When the next call proves every sampler
|
||||||
|
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
|
||||||
|
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
|
||||||
|
// moved, the per-draw glUniform case - the walk collapses to: resolve one
|
||||||
|
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
|
||||||
|
// rebinding the same set with different dynamic offsets).
|
||||||
|
// Invalidation inventory: BeginFrame clears it (the frame's sets are
|
||||||
|
// recycled) and the frameIndex field guards cross-frame confusion on top;
|
||||||
|
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
|
||||||
|
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
|
||||||
|
// program relink bumps the backend state version and thus programObj.hash
|
||||||
|
// so the key misses; the program lifetime id is never reused, so a
|
||||||
|
// deleted-and-recreated program misses; a texture/sampler/binding change
|
||||||
|
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||||
|
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||||
|
// advances, so the recorded set is never re-written within its frame.
|
||||||
|
struct FastRebindMemo {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 frameIndex = 0;
|
||||||
|
Uint64 programLifetimeId = 0;
|
||||||
|
ProgramFactory::HashType programHash = 0;
|
||||||
|
Uint32 uboBinding = 0;
|
||||||
|
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||||
|
VkDeviceSize uboRange = 0;
|
||||||
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
|
};
|
||||||
|
FastRebindMemo m_fastRebindMemo;
|
||||||
|
|
||||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||||
// block resolve to the same set AND the same dynamic offsets, so the
|
// block resolve to the same set AND the same dynamic offsets, so the
|
||||||
@@ -228,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;
|
||||||
@@ -245,7 +380,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||||
Bool viewFormatValid = false;
|
Bool viewFormatValid = false;
|
||||||
|
// Whole resolved descriptor from this binding's last full resolve. Reused
|
||||||
|
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
|
||||||
|
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||||
|
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||||
|
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
||||||
|
//
|
||||||
|
// This one field is keyed by binding but describes ONE descriptor, so it is
|
||||||
|
// written and read only for single-descriptor bindings. A sampler ARRAY's
|
||||||
|
// elements share the binding and would overwrite each other here - the last
|
||||||
|
// element resolved would then be handed to element 0 on the next hinted draw.
|
||||||
|
// Every other field above is self-validating (each compares its full key
|
||||||
|
// before reuse, and the view-format entry is a pure function of format and
|
||||||
|
// numeric domain), so an arrayed binding may keep using those.
|
||||||
|
VkDescriptorImageInfo info{};
|
||||||
|
Bool infoValid = false;
|
||||||
};
|
};
|
||||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
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
|
||||||
|
|||||||
@@ -29,17 +29,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +75,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||||
|
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
|
||||||
|
// memo (pure VALUES derived from the VAO configuration, so config-version
|
||||||
|
// guarding alone is sound). The draw fast path reads them from the VAO object it
|
||||||
|
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
|
||||||
|
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||||
|
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +108,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VkFormat sourceVkFormat =
|
const VkFormat sourceVkFormat =
|
||||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
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);
|
||||||
@@ -114,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),
|
||||||
@@ -124,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);
|
||||||
@@ -135,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);
|
||||||
@@ -158,17 +175,23 @@ 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;
|
||||||
|
// A converted stream is tightly packed, so its stride is the converted element
|
||||||
|
// size - unless the source stride is zero, which does not describe a packing at
|
||||||
|
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||||
|
// converts exactly one element and every vertex reads it.
|
||||||
|
if (sourceStride != 0) {
|
||||||
if (conversion == VertexStreamConversion::Repack) {
|
if (conversion == VertexStreamConversion::Repack) {
|
||||||
stride = static_cast<Uint32>(attribByteSize);
|
stride = static_cast<Uint32>(attribByteSize);
|
||||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
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;
|
||||||
|
|
||||||
@@ -264,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;
|
||||||
}
|
}
|
||||||
@@ -273,7 +298,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||||
Bool isBgra) {
|
Bool isBgra, Bool isLong) {
|
||||||
if (isBgra) {
|
if (isBgra) {
|
||||||
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
||||||
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
||||||
@@ -298,6 +323,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case DataType::Int2101010Rev:
|
case DataType::Int2101010Rev:
|
||||||
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
||||||
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
||||||
|
case DataType::Float64:
|
||||||
|
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
|
||||||
|
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
|
||||||
|
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
|
||||||
|
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||||
|
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||||
|
// so they always agree without extra plumbing.
|
||||||
|
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||||
|
switch (size) {
|
||||||
|
case 1: return VK_FORMAT_R32G32_UINT;
|
||||||
|
case 2: return VK_FORMAT_R32G32B32A32_UINT;
|
||||||
|
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
|
||||||
|
// over two attribute locations, which the location-per-VAO-index model here does not
|
||||||
|
// express. Declined rather than fetched wrong.
|
||||||
|
default: return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
case DataType::Float32:
|
case DataType::Float32:
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 1: return VK_FORMAT_R32_SFLOAT;
|
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -71,6 +72,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
~VertexInputStateFactory() = default;
|
~VertexInputStateFactory() = default;
|
||||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||||
|
|
||||||
|
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
||||||
|
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
||||||
|
// Readers that find the aux memo valid can use these without resolving the entry.
|
||||||
|
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
||||||
|
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
||||||
|
}
|
||||||
|
|
||||||
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||||
// is unchanged. Use this on per-draw paths.
|
// is unchanged. Use this on per-draw paths.
|
||||||
@@ -79,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
|
||||||
@@ -93,17 +103,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
|
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||||
|
Bool isLong = false);
|
||||||
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||||
|
|
||||||
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;
|
||||||
@@ -112,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() {
|
||||||
@@ -242,8 +257,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
||||||
if (m_liveResources.size() >= kLiveResourcePruneThreshold) {
|
// Sweep on a doubling watermark rather than on every insert past the threshold. The old
|
||||||
|
// form walked the whole vector for each new buffer once the list passed 256, and when the
|
||||||
|
// buffers are all live the walk removes nothing and the list grows by one - so creating N
|
||||||
|
// live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep
|
||||||
|
// still removes exactly the expired entries, just less often and with the same bound on how
|
||||||
|
// much dead weight can accumulate (at most as many entries as were live at the last sweep).
|
||||||
|
if (m_liveResources.size() >= std::max<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
|
||||||
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
|
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
|
||||||
|
m_liveResourcesLastPruned = m_liveResources.size();
|
||||||
}
|
}
|
||||||
m_liveResources.push_back(resource);
|
m_liveResources.push_back(resource);
|
||||||
}
|
}
|
||||||
@@ -251,6 +273,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkBufferManager::ReleaseAllLiveResources() {
|
void VkBufferManager::ReleaseAllLiveResources() {
|
||||||
for (auto& weak : m_liveResources) {
|
for (auto& weak : m_liveResources) {
|
||||||
if (auto resource = weak.lock()) {
|
if (auto resource = weak.lock()) {
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->buffer.Destroy();
|
resource->buffer.Destroy();
|
||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->usageFlags = 0;
|
resource->usageFlags = 0;
|
||||||
@@ -265,6 +288,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
|
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
|
||||||
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
|
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
|
||||||
|
// The only place a resident VkBuffer handle is minted, so every resident slice
|
||||||
|
// change funnels through here (callers release the old handle first).
|
||||||
|
BumpSliceEpoch(resource);
|
||||||
// Staged range copies write resident storage with vkCmdCopyBuffer.
|
// Staged range copies write resident storage with vkCmdCopyBuffer.
|
||||||
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
const Bool created = resource.buffer.Create({
|
const Bool created = resource.buffer.Create({
|
||||||
@@ -276,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;
|
||||||
@@ -298,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;
|
||||||
}
|
}
|
||||||
@@ -351,8 +377,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return; // lazy: AcquireResidentSlice performs a full upload on creation
|
return; // lazy: AcquireResidentSlice performs a full upload on creation
|
||||||
}
|
}
|
||||||
|
// A respecify can change the size, the usage hint (so the resident/streamed
|
||||||
|
// route), and the contents at once; retire every memo before deciding what to
|
||||||
|
// do about the storage.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -373,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,6 +419,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Drops the streaming memo below and may end in a storage swap or a deferred
|
||||||
|
// full re-upload, so no memoised slice survives this.
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
||||||
return;
|
return;
|
||||||
@@ -395,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;
|
||||||
@@ -415,6 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
||||||
return;
|
return;
|
||||||
@@ -431,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;
|
||||||
@@ -474,6 +514,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
TrackLiveResource(resource);
|
TrackLiveResource(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bumped for the request, not just for the storage it may create. This is the
|
||||||
|
// one call the frontend makes when a buffer becomes persistently mapped for
|
||||||
|
// writing (BufferObject::AcquireMemoryRange), and a map the backend declines
|
||||||
|
// keeps mutating its shadow with no further API call - so it is what lets
|
||||||
|
// GetSliceEpochCounter stand for "no buffer needs a persistent-map range push".
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
|
|
||||||
// Idempotent: an already-backed buffer returns the same mapped base.
|
// Idempotent: an already-backed buffer returns the same mapped base.
|
||||||
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
|
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
|
||||||
return resource->buffer.GetMappedData();
|
return resource->buffer.GetMappedData();
|
||||||
@@ -516,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,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;
|
||||||
@@ -573,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +631,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle-content promotion: see the field comments in VkBufferResource. The
|
||||||
|
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
|
||||||
|
// above swallows repeat draws), so a promotion needs the content stable
|
||||||
|
// for kStreamedPromotionStreak whole frames - one no-op frame does not
|
||||||
|
// trigger the resident round-trip, whose creation upload is itself a
|
||||||
|
// staged copy worth avoiding for content that is about to change again.
|
||||||
|
constexpr Uint32 kStreamedPromotionStreak = 2;
|
||||||
|
if (resource->promotedResident) {
|
||||||
|
if (resource->promotedChangeSerial == changeSerial &&
|
||||||
|
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
|
||||||
|
return AcquireResidentSlice(kind, bufferObject, outSlice);
|
||||||
|
}
|
||||||
|
resource->promotedResident = false;
|
||||||
|
resource->unchangedStreak = 0;
|
||||||
|
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
|
||||||
|
resource->transientFrameSerial != 0) {
|
||||||
|
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
|
||||||
|
// Promotion moves the buffer off the arena and onto resident storage.
|
||||||
|
resource->promotedResident = true;
|
||||||
|
resource->promotedChangeSerial = changeSerial;
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
|
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
resource->promotedResident = false; // resident creation failed: stream as before
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resource->unchangedStreak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh arena allocation: a different slice than the last call handed back,
|
||||||
|
// and (below) the point where a promoted buffer's resident storage is released.
|
||||||
|
// The stable-promotion exit above returns before this, so a buffer the app has
|
||||||
|
// stopped touching keeps one slice for as long as it keeps its resident storage.
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
|
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
|
||||||
outSlice)) {
|
outSlice)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -636,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:
|
||||||
|
|||||||
@@ -57,11 +57,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
||||||
Bool persistentMapped = false;
|
Bool persistentMapped = false;
|
||||||
|
|
||||||
|
// Bumped from a manager-wide counter every time anything that decides which
|
||||||
|
// BufferSlice an Acquire*Slice call hands back changes: storage created or
|
||||||
|
// released, a full re-upload becoming due, a promotion/demotion between
|
||||||
|
// resident and streamed storage, or a new per-frame arena slice. Callers that
|
||||||
|
// memoise a resolved slice compare this to prove the memo still describes the
|
||||||
|
// buffer. The counter is manager-wide (never per-resource) so a freshly
|
||||||
|
// created resource - including one that replaces a destroyed resource at the
|
||||||
|
// same address - can never reproduce a value some memo already holds. 0 means
|
||||||
|
// "no slice has ever been handed out", which no memo can match.
|
||||||
|
Uint64 sliceEpoch = 0;
|
||||||
|
|
||||||
// Cached transient (streaming) slice for the current frame.
|
// Cached transient (streaming) slice for the current frame.
|
||||||
BufferSlice transientSlice{};
|
BufferSlice transientSlice{};
|
||||||
Uint64 transientFrameSerial = 0;
|
Uint64 transientFrameSerial = 0;
|
||||||
Uint64 transientChangeSerial = 0;
|
Uint64 transientChangeSerial = 0;
|
||||||
VkDeviceSize transientSize = 0;
|
VkDeviceSize transientSize = 0;
|
||||||
|
|
||||||
|
// Streaming re-copies the whole store into the per-frame arena on every
|
||||||
|
// frame, which is right for genuinely per-frame data but pure waste for a
|
||||||
|
// Dynamic-hinted buffer the app stopped touching. After the content
|
||||||
|
// survives kStreamedPromotionStreak frame boundaries unchanged it is
|
||||||
|
// promoted to resident storage (one final upload, then zero per-frame
|
||||||
|
// cost); the first content change demotes it back to streaming, and the
|
||||||
|
// streaming path's existing downgrade releases the resident store.
|
||||||
|
Uint32 unchangedStreak = 0;
|
||||||
|
Bool promotedResident = false;
|
||||||
|
Uint64 promotedChangeSerial = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supplies a command buffer that is recording and outside any render pass,
|
// Supplies a command buffer that is recording and outside any render pass,
|
||||||
@@ -80,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();
|
||||||
@@ -121,6 +144,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||||
|
|
||||||
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
||||||
|
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
|
||||||
|
// memo was taken means no buffer this manager owns changed which slice it hands
|
||||||
|
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||||
|
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||||
|
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||||
// Highest frame serial whose GPU work is known complete; serials at or
|
// Highest frame serial whose GPU work is known complete; serials at or
|
||||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||||
// backend GL fence objects.
|
// backend GL fence objects.
|
||||||
@@ -147,6 +175,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void DestroyAllDeferredReleases();
|
void DestroyAllDeferredReleases();
|
||||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||||
void ReleaseAllLiveResources();
|
void ReleaseAllLiveResources();
|
||||||
|
// See VkBufferResource::sliceEpoch.
|
||||||
|
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
|
||||||
|
|
||||||
VkBufferManagerInitInfo m_initInfo{};
|
VkBufferManagerInitInfo m_initInfo{};
|
||||||
BufferArena m_transientUploadArena;
|
BufferArena m_transientUploadArena;
|
||||||
@@ -154,8 +184,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||||
|
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
|
||||||
|
SizeT m_liveResourcesLastPruned = 0;
|
||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
Uint64 m_frameSerial = 1;
|
Uint64 m_frameSerial = 1;
|
||||||
Uint64 m_completedSerialFloor = 0;
|
Uint64 m_completedSerialFloor = 0;
|
||||||
|
// Never reset (not even by Shutdown): a value handed to a resource must stay
|
||||||
|
// unique for the process, or a memo taken before a re-initialize could match
|
||||||
|
// a different resource's state after it.
|
||||||
|
Uint64 m_sliceEpochCounter = 0;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -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,22 +170,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
|
||||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
|
||||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
|
||||||
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
|
||||||
|
|
||||||
BufferSlice slice{};
|
|
||||||
slice.buffer = m_buffer;
|
|
||||||
slice.offset = offset;
|
|
||||||
slice.size = resolvedSize;
|
|
||||||
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -48,7 +48,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkBuffer GetHandle() const { return m_buffer; }
|
VkBuffer GetHandle() const { return m_buffer; }
|
||||||
VkDeviceSize GetSize() const { return m_size; }
|
VkDeviceSize GetSize() const { return m_size; }
|
||||||
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
|
// Inline: runs on the per-draw acquire path (a resident buffer bind is a
|
||||||
|
// GetSlice per binding), where an out-of-line call was measurable.
|
||||||
|
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const {
|
||||||
|
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||||
|
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||||
|
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
||||||
|
|
||||||
|
BufferSlice slice{};
|
||||||
|
slice.buffer = m_buffer;
|
||||||
|
slice.offset = offset;
|
||||||
|
slice.size = resolvedSize;
|
||||||
|
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
void* GetMappedData() const { return m_mappedData; }
|
void* GetMappedData() const { return m_mappedData; }
|
||||||
Bool IsMapped() const { return m_mappedData != nullptr; }
|
Bool IsMapped() const { return m_mappedData != nullptr; }
|
||||||
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -87,9 +87,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkImageViewType ResolveAttachmentViewType(
|
static VkImageViewType ResolveAttachmentViewType(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||||
const VkTextureManager::TextureResource& resource) {
|
const VkTextureManager::TextureResource& resource) {
|
||||||
return !attachment.IsLayered() && IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ?
|
if (attachment.IsLayered()) {
|
||||||
VK_IMAGE_VIEW_TYPE_2D :
|
return resource.viewType;
|
||||||
resource.viewType;
|
}
|
||||||
|
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||||
|
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||||
|
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
|
||||||
|
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||||
|
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||||
|
// symmetry with CUBE_ARRAY.
|
||||||
|
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||||
|
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||||
|
return VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
}
|
||||||
|
return resource.viewType;
|
||||||
}
|
}
|
||||||
|
|
||||||
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
||||||
@@ -112,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());
|
||||||
@@ -121,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());
|
||||||
@@ -300,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;
|
||||||
@@ -446,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());
|
||||||
@@ -820,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 &&
|
||||||
@@ -844,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();
|
||||||
@@ -918,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;
|
||||||
@@ -1094,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;
|
||||||
@@ -1150,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) {
|
||||||
@@ -1212,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);
|
||||||
}
|
}
|
||||||
@@ -1496,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) {
|
||||||
|
// 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;
|
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)) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -100,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,
|
||||||
@@ -252,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;
|
||||||
@@ -314,7 +356,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 deferredAtFrame = 0;
|
Uint64 deferredAtFrame = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
|
||||||
|
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||||
|
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||||
|
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||||
|
// materializes the source's pending clear, which looks that same resource up again. Growing
|
||||||
|
// an open-addressed table relocates every element, so the cached pointer went on to name
|
||||||
|
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
|
||||||
|
// out at "source image layout is undefined", silently dropping the blit -
|
||||||
|
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
|
||||||
|
// iterations that grew the table.
|
||||||
|
//
|
||||||
|
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||||
|
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||||
|
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||||
|
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||||
|
// ordering rule. m_textureResources is node-based for the same reason.
|
||||||
|
//
|
||||||
|
// The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
|
||||||
|
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
|
||||||
|
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
|
||||||
|
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
|
||||||
|
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
|
||||||
|
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
|
||||||
|
// That narrows the insert hazard; it does not touch the erase one.)
|
||||||
|
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,17 @@ public:
|
|||||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||||
Bool imageFormatListSupported = false;
|
Bool imageFormatListSupported = false;
|
||||||
|
// Union of shader stages sampled-read barriers may name on this device; the renderer
|
||||||
|
// builds it from the enabled features because geometry/tessellation stage bits are
|
||||||
|
// invalid in a barrier when their feature is off.
|
||||||
|
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
|
// Family of `graphicsQueue`; the manager creates its own command pool
|
||||||
|
// on it for the recycled upload-batch command buffers, so their parked
|
||||||
|
// allocations never sit in (and fragment) the renderer's shared pool
|
||||||
|
// that frame command buffers churn through every frame.
|
||||||
|
Uint32 graphicsQueueFamilyIndex = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TextureResource {
|
struct TextureResource {
|
||||||
@@ -299,9 +310,22 @@ 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);
|
||||||
|
// Submits the accumulated texture-upload batch (one command buffer, one
|
||||||
|
// vkQueueSubmit, one pooled fence) if any uploads are pending. MUST run
|
||||||
|
// before any other vkQueueSubmit on the shared graphics queue whose
|
||||||
|
// commands may consume an image the batch writes - the frame command
|
||||||
|
// buffer submit (mid-frame flush, readback, Present) and the
|
||||||
|
// preserve-on-recreate copy are the existing callers. No-op when the
|
||||||
|
// batch is empty.
|
||||||
|
void FlushPendingUploads();
|
||||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||||
// the caller has proven every queue submission complete; used by the
|
// the caller has proven every queue submission complete; used by the
|
||||||
// present-less frame-boundary drain.
|
// present-less frame-boundary drain.
|
||||||
@@ -324,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
|
||||||
@@ -360,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();
|
||||||
|
|
||||||
@@ -447,6 +494,9 @@ private:
|
|||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
VmaAllocator m_allocator = nullptr;
|
VmaAllocator m_allocator = nullptr;
|
||||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||||
|
// Dedicated pool for the recycled upload-batch command buffers (see
|
||||||
|
// InitInfo::graphicsQueueFamilyIndex).
|
||||||
|
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
|
||||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||||
Bool m_imageFormatListSupported = false;
|
Bool m_imageFormatListSupported = false;
|
||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
@@ -487,6 +537,10 @@ private:
|
|||||||
// Formats whose mutable-image probe failed on this device; their images are created
|
// Formats whose mutable-image probe failed on this device; their images are created
|
||||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||||
|
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
|
||||||
|
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
||||||
|
// remembered rather than probed once globally.
|
||||||
|
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||||
@@ -496,15 +550,58 @@ private:
|
|||||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||||
|
|
||||||
|
// --- Batched upload machinery ---
|
||||||
|
// Uploads within a frame are recorded into ONE shared command buffer and
|
||||||
|
// submitted with ONE vkQueueSubmit at FlushPendingUploads (the renderer
|
||||||
|
// flushes before every frame-command-buffer submit). Staging memory comes
|
||||||
|
// from a pool of persistently-mapped, reusable blocks instead of a
|
||||||
|
// vmaCreateBuffer per upload.
|
||||||
|
struct UploadStagingBlock {
|
||||||
|
VkBuffer buffer = VK_NULL_HANDLE;
|
||||||
|
VmaAllocation allocation = nullptr;
|
||||||
|
Uint8* mapped = nullptr; // persistently mapped for the block's lifetime
|
||||||
|
VkDeviceSize capacity = 0;
|
||||||
|
VkDeviceSize cursor = 0; // bump cursor while the block backs the open batch
|
||||||
|
};
|
||||||
|
// Opens the batch command buffer lazily (allocates/reuses + begins recording).
|
||||||
|
VkCommandBuffer EnsureUploadBatchOpen();
|
||||||
|
// Bump-allocates `size` staging bytes for the open batch, growing onto a
|
||||||
|
// new/pooled block when the current one cannot fit. Returns the write
|
||||||
|
// pointer; outBuffer/outBaseOffset locate the space for copy commands.
|
||||||
|
Uint8* AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer, VkDeviceSize& outBaseOffset);
|
||||||
|
void RecycleUploadStagingBlock(UploadStagingBlock&& block);
|
||||||
|
// Drops a recorded-but-unsubmitted batch on the floor. Shutdown only: the
|
||||||
|
// device is being torn down, so the lost texel data is unobservable.
|
||||||
|
void DiscardPendingUploadBatch();
|
||||||
|
void DestroyUploadPools();
|
||||||
|
|
||||||
|
Vector<UploadStagingBlock> m_freeUploadStagingBlocks;
|
||||||
|
VkDeviceSize m_freeUploadStagingBytes = 0;
|
||||||
|
Vector<VkCommandBuffer> m_freeUploadCommandBuffers;
|
||||||
|
Vector<VkFence> m_freeUploadFences;
|
||||||
|
Bool m_uploadBatchOpen = false;
|
||||||
|
VkCommandBuffer m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
|
||||||
|
// Blocks whose staging bytes the open batch's copies reference (last =
|
||||||
|
// the block the bump cursor is currently allocating from).
|
||||||
|
Vector<UploadStagingBlock> m_uploadBatchBlocks;
|
||||||
|
// Images the open batch writes; consulted for the rare re-upload-after-
|
||||||
|
// draw flush and by DeferResourceRelease (an unsubmitted command buffer
|
||||||
|
// referencing a deferred-released image would escape every fence-based
|
||||||
|
// destruction proof, so the batch is flushed before the image is parked).
|
||||||
|
Vector<VkImage> m_uploadBatchImages;
|
||||||
|
VkDeviceSize m_uploadBatchStagingBytes = 0;
|
||||||
|
|
||||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||||
// behind the queue serialized the CPU against the previous frame's GPU
|
// behind the queue serialized the CPU against the previous frame's GPU
|
||||||
// work every time an animated atlas re-uploaded). Their transient objects
|
// work every time an animated atlas re-uploaded). Each flushed batch's
|
||||||
// are parked here and reclaimed once the upload fence signals.
|
// transients are parked here and RECYCLED (fence reset to the fence pool,
|
||||||
|
// command buffer reset to the CB pool, staging blocks back to the block
|
||||||
|
// pool) once the batch fence signals.
|
||||||
struct PendingUploadReclaim {
|
struct PendingUploadReclaim {
|
||||||
VkFence fence = VK_NULL_HANDLE;
|
VkFence fence = VK_NULL_HANDLE;
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
Vector<UploadStagingBlock> stagingBlocks;
|
||||||
VmaAllocation stagingAllocation = nullptr;
|
|
||||||
};
|
};
|
||||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,19 @@ 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
|
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||||
// owns that window and must destroy it.
|
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||||
Bool m_ownsFallbackXlibWindow = false;
|
// displayed - the reader's images are simply never acquired. Owned here, so
|
||||||
|
// Shutdown() deletes it.
|
||||||
|
void* m_fallbackImageReader = nullptr;
|
||||||
VulkanRendererConfig m_config;
|
VulkanRendererConfig m_config;
|
||||||
Bool m_swapchainResizeRequested = false;
|
Bool m_swapchainResizeRequested = false;
|
||||||
// Presentation is suspended while the window is zero-area (minimized): the
|
// Presentation is suspended while the window is zero-area (minimized): the
|
||||||
@@ -471,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.
|
||||||
@@ -485,6 +578,19 @@ 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
|
||||||
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
|
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||||
|
// ALL_GRAPHICS would also serialize against non-shader stages.
|
||||||
|
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
// Cached at device creation from the graphics queue family properties
|
// Cached at device creation from the graphics queue family properties
|
||||||
// and device limits; drives timer-query support.
|
// and device limits; drives timer-query support.
|
||||||
Uint32 m_timestampValidBits = 0;
|
Uint32 m_timestampValidBits = 0;
|
||||||
@@ -495,9 +601,37 @@ 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;
|
||||||
|
// VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
|
||||||
|
// default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
|
||||||
|
// feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
|
||||||
|
// input-assembler capture order and has no dependency on transform feedback; only
|
||||||
|
// transformFeedbackPreservesProvokingVertex does.
|
||||||
|
Bool m_provokingVertexLastEnabled = false;
|
||||||
|
// transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
|
||||||
|
// separate because it is the only thing that arms
|
||||||
|
// VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
|
||||||
|
// pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
|
||||||
|
Bool m_provokingVertexXfbPreserveEnabled = false;
|
||||||
|
// provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
|
||||||
|
// must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
|
||||||
|
// per draw and every pipeline takes GL's default (LAST) instead.
|
||||||
|
Bool m_provokingVertexModePerPipeline = false;
|
||||||
|
// transformFeedbackPreservesTriangleFanProvokingVertex.
|
||||||
|
Bool m_provokingVertexFanPreserved = false;
|
||||||
|
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
||||||
|
// property of the program, never the dynamic "is transform feedback active" flag: the
|
||||||
|
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
||||||
|
// the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
|
||||||
|
// called, so a dynamic input here would hand back a stale VkPipeline.
|
||||||
|
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||||
|
Bool capturesXfbFromGeometryStage) const;
|
||||||
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
||||||
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
||||||
Bool m_vertexAttributeDivisorEnabled = false;
|
Bool m_vertexAttributeDivisorEnabled = false;
|
||||||
@@ -586,7 +720,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 programHash = 0;
|
Uint64 programHash = 0;
|
||||||
Uint64 vertexInputHash = 0;
|
Uint64 vertexInputHash = 0;
|
||||||
Uint64 renderPassHash = 0;
|
Uint64 renderPassHash = 0;
|
||||||
Uint renderStateVersion = 0;
|
// VALUE hash of the pipeline-relevant fixed-function state (see
|
||||||
|
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
||||||
|
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
||||||
|
// all entries forever even though the state alternates between two
|
||||||
|
// values the memo already holds.
|
||||||
|
Uint64 pipelineStateHash = 0;
|
||||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
@@ -594,11 +733,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||||
Uint32 m_pipelineMemoCount = 0;
|
Uint32 m_pipelineMemoCount = 0;
|
||||||
Uint32 m_pipelineMemoNext = 0;
|
Uint32 m_pipelineMemoNext = 0;
|
||||||
|
// Hash of every fixed-function GL state the pipeline payload reads that the
|
||||||
|
// memo key's other fields (mode / program / vertex input / render pass /
|
||||||
|
// transform flags) do not already pin down. Equal hash under an equal rest
|
||||||
|
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||||
|
// version: the version is monotonic and bumps on every pipeline-state
|
||||||
|
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||||
|
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||||
|
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||||
|
Uint m_pipelineStateHashVersion = 0;
|
||||||
|
Uint32 m_pipelineStateHashColorCount = 0;
|
||||||
|
Uint64 m_pipelineStateHash = 0;
|
||||||
|
Bool m_pipelineStateHashValid = false;
|
||||||
|
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||||
|
// function also reads whether the bound DRAW framebuffer is the default one
|
||||||
|
// (only the default framebuffer gets the Y-flip and rotation bits - an FBO
|
||||||
|
// pass renders unflipped). Keyed on BOTH inputs; missing the FBO bit shipped
|
||||||
|
// an upside-down default-framebuffer pass after any render-to-texture
|
||||||
|
// (minecraft-1.17-main-menu retrace, whole frame flipped).
|
||||||
|
VkSurfaceTransformFlagBitsKHR m_baseTransformFlagsPreTransform =
|
||||||
|
VK_SURFACE_TRANSFORM_FLAG_BITS_MAX_ENUM_KHR;
|
||||||
|
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
||||||
|
Bool m_baseTransformFlagsKeyValid = false;
|
||||||
|
Uint32 m_baseTransformFlagsCache = 0;
|
||||||
|
// 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.
|
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||||
|
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||||
|
// context whose monotonic version the cache is keyed on.
|
||||||
void InvalidatePipelineMemo() {
|
void InvalidatePipelineMemo() {
|
||||||
m_pipelineMemoCount = 0;
|
m_pipelineMemoCount = 0;
|
||||||
m_pipelineMemoNext = 0;
|
m_pipelineMemoNext = 0;
|
||||||
|
m_pipelineStateHashValid = false;
|
||||||
}
|
}
|
||||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||||
UniquePtr<ProgramFactory> m_programFactory;
|
UniquePtr<ProgramFactory> m_programFactory;
|
||||||
@@ -639,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
|
||||||
@@ -655,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;
|
||||||
@@ -670,20 +866,90 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 renderbufferImageEpoch = 0;
|
Uint64 renderbufferImageEpoch = 0;
|
||||||
Uint64 sampledContentSum = 0;
|
Uint64 sampledContentSum = 0;
|
||||||
Uint64 sampledParamsSum = 0;
|
Uint64 sampledParamsSum = 0;
|
||||||
|
// Guards the sampler-descriptor reuse hint: bumped by any sampler-object
|
||||||
|
// parameter or texture shape change (see GetSamplingResolutionGeneration),
|
||||||
|
// none of which the sums above cover.
|
||||||
|
Uint64 samplingResolutionGeneration = 0;
|
||||||
|
// Render-pass flavor input (DepthTest || StencilTest at snapshot time).
|
||||||
|
// A pipeline-state change that leaves this equal cannot change which
|
||||||
|
// render pass GetOrCreateRenderPass would pick, so the fast path may
|
||||||
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
|
// flips it must fall back to the full path's pass selection.
|
||||||
|
Bool drawUsesDepthStencil = false;
|
||||||
|
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||||
|
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||||
|
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||||
|
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||||
|
Uint32 viewportCount = 1;
|
||||||
IntVec2 renderPassExtent = {0, 0};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
|
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||||
|
// probe the pipeline memo after a state change without re-fetching the
|
||||||
|
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||||
|
Uint32 renderPassColorCount = 0;
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||||
|
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||||
|
// program, pinned separately), so a changed VAO whose aux memo carries the
|
||||||
|
// same layoutHash re-uses the snapshot's pipeline and pre-flight verdict
|
||||||
|
// outright - the VAO-cycling case Minecraft chunk rendering hits every draw.
|
||||||
|
Uint64 vaoLayoutHash = 0;
|
||||||
|
// Memoised ProgramFactory entry of the snapshotting draw, valid while
|
||||||
|
// (programLifetimeId, programVersion, resolvedTransformFlags) match - all
|
||||||
|
// checked above - AND the factory's cache structure epoch is unchanged (the
|
||||||
|
// cache is open-addressing and holds entries by value, so any insert/erase
|
||||||
|
// moves them). The fast path must re-stamp use through StampProgramUse when
|
||||||
|
// it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry.
|
||||||
|
const ProgramFactory::VkProgramObject* programObj = nullptr;
|
||||||
|
Uint64 programFactoryEpoch = 0;
|
||||||
|
// Per-entry copies of the snapshotting draw's sampled set (the scratch
|
||||||
|
// vectors below hold only the LAST full-path draw's set, which with more
|
||||||
|
// than one snapshot entry is not necessarily this entry's program).
|
||||||
|
// sampledTextures/sampledResources carry the same epoch-guarded pointer
|
||||||
|
// lifetime rules as the scratch originals: textureEraseEpoch (checked
|
||||||
|
// every probe) declines the entry before any erased resource pointer
|
||||||
|
// could be dereferenced. sampledLayouts is the layout VALUE each
|
||||||
|
// resource held when this entry's descriptors were built (the
|
||||||
|
// descriptor-reuse hint needs the SAME layout, not just a sampleable
|
||||||
|
// one), and sampledBindingRecords feeds SampledBindingsUnchanged when
|
||||||
|
// the bind generation moved.
|
||||||
|
Vector<MG_State::GLState::ITextureObject*> sampledTextures;
|
||||||
|
Vector<VkTextureManager::TextureResource*> sampledResources;
|
||||||
|
Vector<VkImageLayout> sampledLayouts;
|
||||||
|
Vector<UniformManager::SampledBindingRecord> sampledBindingRecords;
|
||||||
};
|
};
|
||||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
// Program-keyed snapshot entries: program ping-pong (Sodium switches programs
|
||||||
|
// mid-frame every few draws) would otherwise evict the single snapshot on
|
||||||
|
// every switch and send every draw through the full path. Entries are found
|
||||||
|
// by programLifetimeId (MRU-first probe); every other guard stays per-probe,
|
||||||
|
// so a stale entry declines itself exactly like the old single snapshot did.
|
||||||
|
static constexpr Uint32 kSetupDrawSnapshotCount = 4;
|
||||||
|
SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount];
|
||||||
|
Uint32 m_setupDrawSnapshotMru = 0; // last entry that hit or was filled
|
||||||
|
Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live
|
||||||
|
void InvalidateSetupDrawSnapshots() {
|
||||||
|
for (auto& snapshot : m_setupDrawSnapshots) {
|
||||||
|
snapshot.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||||
// draw call and must not allocate.
|
// draw call and must not allocate.
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||||
|
// Per-binding (texture, effective sampler) lifetime-id records from the same
|
||||||
|
// CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast
|
||||||
|
// path shadow-compares against them (SampledBindingsUnchanged) when the
|
||||||
|
// texture bind generation moved, so a redundant glBindSampler/glBindTexture
|
||||||
|
// storm that resolves to the same bindings keeps the fast path.
|
||||||
|
Vector<UniformManager::SampledBindingRecord> m_sampledBindingRecordsScratch;
|
||||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||||
// first sampled-texture loop: the resolved backend resources, so the
|
// first sampled-texture loop: the resolved backend resources, so the
|
||||||
// post-transition loop can skip re-resolving textures whose layout is
|
// post-transition loop can skip re-resolving textures whose layout is
|
||||||
// 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;
|
||||||
@@ -742,6 +1008,146 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
||||||
m_convertedVertexStreams;
|
m_convertedVertexStreams;
|
||||||
|
|
||||||
|
// One VAO's resolved vkCmdBindVertexBuffers arguments, reusable by a later draw
|
||||||
|
// that would resolve them to the same thing. Consecutive draws in a chunk-renderer
|
||||||
|
// frame keep the program and the vertex layout and only swap the VAO, so a
|
||||||
|
// per-VAO memo turns the second and later draws through each VAO into a validate
|
||||||
|
// plus (usually skipped) rebind.
|
||||||
|
//
|
||||||
|
// Only whole-buffer bindings are memoised. Client-memory and format-converted
|
||||||
|
// streams re-upload from a range that depends on the draw's own vertex/index
|
||||||
|
// range, and synthetic bindings carry glVertexAttrib* values that are not part
|
||||||
|
// of any key here; a layout using any of them is never stored.
|
||||||
|
// Field order is hit-path cache locality, hot to cold: the per-draw validate
|
||||||
|
// reads the scalars and the EBO memo head, then only the first bindingCount
|
||||||
|
// elements of vkBuffers/vkOffsets; the per-binding revalidation arrays at the
|
||||||
|
// tail are touched once per frame at most.
|
||||||
|
struct ResolvedVertexBindings {
|
||||||
|
// Must equal DynamicStateShadow::kMaxShadowedVertexBindings (static_assert in
|
||||||
|
// the .cpp): past that width the bind shadow cannot skip a redundant bind
|
||||||
|
// either, so a wider layout resolves per draw. Minecraft-shaped layouts use four.
|
||||||
|
static constexpr Uint32 kMaxBindings = 8;
|
||||||
|
|
||||||
|
// Frame serial of the last completed resolve OR cross-frame revalidation.
|
||||||
|
// Zero until a resolve completes, and reset to zero before one starts, so a
|
||||||
|
// resolve that bails out midway cannot leave a half-filled entry matchable.
|
||||||
|
// Unlike the original frame-scoped memo, an entry whose buffers are all
|
||||||
|
// resident and unmapped is revalidated across frames (per-binding slice
|
||||||
|
// epoch compares) instead of re-resolved - see TryBindResolvedVertexBindings.
|
||||||
|
Uint64 frameSerial = 0;
|
||||||
|
// Identity of the resolved Vulkan layout: the VAO's content hash
|
||||||
|
// (VertexInputStateFactory::GetOrComputeHash - the same value the factory
|
||||||
|
// keys its entries on) fixes bindings.size(), each binding's base offset,
|
||||||
|
// which bindings are client/converted, and (through the mixed-in buffer
|
||||||
|
// addresses) which buffer each binding reads. Compared against the VAO's
|
||||||
|
// own hash memo on the hit path, so a hit never touches the factory entry.
|
||||||
|
VertexInputStateFactory::HashType vertexInputHash = 0;
|
||||||
|
// The program's vertex input layout: decides the synthetic-binding set and
|
||||||
|
// hence the total binding count.
|
||||||
|
Uint32 activeAttribMask = 0;
|
||||||
|
Uint32 bindingCount = 0;
|
||||||
|
// VkBufferManager::GetSliceEpochCounter() at resolve time. Still equal means
|
||||||
|
// no buffer anywhere changed its slice or was persistently mapped since, which
|
||||||
|
// settles every per-binding question below in one compare.
|
||||||
|
Uint64 sliceEpochCounter = 0;
|
||||||
|
// Any bound buffer already carrying a host map when the slice was resolved.
|
||||||
|
// Such a buffer can mutate its shadow with no API call, so it has to be
|
||||||
|
// re-pushed per draw and the one-compare path above cannot apply.
|
||||||
|
Bool anyBufferMapped = true;
|
||||||
|
|
||||||
|
// Resident element-buffer slice memo (skips the per-draw AcquireResidentSlice
|
||||||
|
// for the VAO's EBO, which cold-chases 500+ distinct resources in a
|
||||||
|
// chunk-cycling frame). Self-validating exactly like the bindings above: a hit
|
||||||
|
// requires the LIVE bound EBO pointer to equal indexBuffer AND either an
|
||||||
|
// unmoved manager-wide slice-epoch counter (nothing anywhere changed slices
|
||||||
|
// or gained a host map, the same one-compare rescue the vertex half uses) or
|
||||||
|
// that buffer's resource still carrying indexSliceEpoch (epochs are minted
|
||||||
|
// from a process-lifetime counter, so a recycled address can never
|
||||||
|
// revalidate). Restart-substituted and streamed EBOs are never stored.
|
||||||
|
// indexFrameSerial tracks the last frame the resource's GPU-use serial was
|
||||||
|
// stamped through this memo; 0 means no index memo. Independent of the
|
||||||
|
// vertex half: both are (pointer, epoch)-validated, so neither can serve
|
||||||
|
// stale state for the other.
|
||||||
|
const MG_State::GLState::BufferObject* indexBuffer = nullptr;
|
||||||
|
Uint64 indexSliceEpoch = 0;
|
||||||
|
// GetSliceEpochCounter() when the resource's epoch was last verified; only
|
||||||
|
// meaningful while indexFrameSerial matches the current frame serial.
|
||||||
|
Uint64 indexSliceEpochCounter = 0;
|
||||||
|
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||||
|
VkDeviceSize indexSliceOffset = 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).
|
||||||
|
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||||
|
VkDeviceSize vkOffsets[kMaxBindings] = {};
|
||||||
|
// Per binding: the VAO attribute location its buffer comes from, that buffer,
|
||||||
|
// and the buffer's VkBufferManager slice epoch when the slice was resolved.
|
||||||
|
// Only read by the per-frame revalidation and the something-moved fallback.
|
||||||
|
Uint8 attributeLocations[kMaxBindings] = {};
|
||||||
|
const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
|
||||||
|
Uint64 sliceEpochs[kMaxBindings] = {};
|
||||||
|
};
|
||||||
|
// One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to
|
||||||
|
// the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone
|
||||||
|
// 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
|
||||||
|
// VAO's own hash memo (which the VAO's config version guards), so a config
|
||||||
|
// change or a buffer rebind misses even for the same object.
|
||||||
|
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||||
|
// hash, per-binding live buffer pointers and slice epochs).
|
||||||
|
struct alignas(64) VaoDrawMemo {
|
||||||
|
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
|
||||||
|
// layout facts below were derived from; 0 while nothing valid is stored.
|
||||||
|
Uint64 contentHash = 0;
|
||||||
|
Bool layoutFactsValid = false;
|
||||||
|
// The resolved layout identity + packed (unsupported, location) masks -
|
||||||
|
// the exact values GetBackendAuxMemo used to serve, moved here so the
|
||||||
|
// per-draw probe stays inside this table's one hot line instead of
|
||||||
|
// touching a second cold line of every cycled VAO object.
|
||||||
|
Uint64 layoutHash = 0;
|
||||||
|
Uint64 layoutAuxMasks = 0;
|
||||||
|
ResolvedVertexBindings bindings;
|
||||||
|
};
|
||||||
|
// Fixed-size, allocated on first use, never rehashed or swept: entries are
|
||||||
|
// recycled in place on slot collisions (two-slot probe, older frame serial
|
||||||
|
// evicted), and stale entries self-invalidate through the compares above. A
|
||||||
|
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||||
|
// stable for the duration of a draw, which the EBO memo handoff
|
||||||
|
// (m_currentDrawResolvedEntry) relies on.
|
||||||
|
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||||
|
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||||
|
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||||
|
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||||
|
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||||
|
// The current draw's memo entry, set by UploadAndBindVertexBuffers and consumed
|
||||||
|
// by the same draw's UploadAndBindIndexBuffer (the EBO memo lives in the same
|
||||||
|
// entry). Valid ONLY within that window: the next draw's lookup can recycle the
|
||||||
|
// slot. Null when the draw's layout is not memoisable.
|
||||||
|
ResolvedVertexBindings* m_currentDrawResolvedEntry = nullptr;
|
||||||
|
|
||||||
void CreateInstance();
|
void CreateInstance();
|
||||||
VkResult SetupDebugMessenger();
|
VkResult SetupDebugMessenger();
|
||||||
VkResult DestroyDebugMessenger();
|
VkResult DestroyDebugMessenger();
|
||||||
@@ -771,11 +1177,49 @@ 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
|
||||||
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
|
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||||
|
// 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,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView);
|
const IndexBufferView* pIndexBufferView);
|
||||||
|
// Binds `entry`'s memoised buffers when every input it was resolved from is
|
||||||
|
// still live and unchanged, else returns false and leaves nothing bound.
|
||||||
|
// vaoContentHash is the VAO's memoised content hash (GetBackendHashMemo), which
|
||||||
|
// pins the layout AND the bound buffers without resolving the factory entry.
|
||||||
|
// Non-const entry: a cross-frame revalidation refreshes its serial/epoch stamps.
|
||||||
|
Bool TryBindResolvedVertexBindings(VkCommandBuffer commandBuffer,
|
||||||
|
const MG_State::GLState::VertexArrayObject& vao,
|
||||||
|
ResolvedVertexBindings& entry,
|
||||||
|
Uint64 vaoContentHash,
|
||||||
|
Uint32 activeAttribMask, Uint64 frameSerial);
|
||||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::VertexArrayObject& vao,
|
const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
const IndexBufferView* pIndexBufferView = nullptr);
|
||||||
@@ -791,11 +1235,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||||
GLenum filter);
|
GLenum filter);
|
||||||
|
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
|
||||||
|
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
|
||||||
|
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||||
|
Uint32 depthSlice, const VkClearValue& clearValue);
|
||||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||||
MG_State::GLState::ITextureObject& texture);
|
MG_State::GLState::ITextureObject& texture);
|
||||||
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,3 +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(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();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
|
||||||
|
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
|
||||||
|
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||||
|
# MobileGL backend), so the same binary measures all three stacks.
|
||||||
|
if (NOT UNIX OR APPLE OR ANDROID)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(DriverBench DriverBench.c)
|
||||||
|
target_link_libraries(DriverBench PRIVATE dl)
|
||||||
|
|
||||||
|
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||||
|
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBench.c
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Headless, EGL-based driver benchmark shaped like Minecraft's GL usage.
|
||||||
|
* Unlike the MobileGL_s microbenches next door this exercises a full GL
|
||||||
|
* stack: it dlopens ONE EGL provider ($DRIVERBENCH_EGL_LIB - the system
|
||||||
|
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||||
|
* MobileGL backend selected with MOBILEGL_BACKEND_TYPE), creates a desktop-GL
|
||||||
|
* context on a small pbuffer, renders into its own FBO and paces frames with
|
||||||
|
* glFinish. No window system is required: the default display is tried first
|
||||||
|
* so a desktop run reaches the real driver, and a headless box (CI, a build
|
||||||
|
* server) falls back to EGL_MESA_platform_surfaceless - see
|
||||||
|
* run_driver_bench.sh.
|
||||||
|
*
|
||||||
|
* Every case models one hot pattern from captured Minecraft traces:
|
||||||
|
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
|
||||||
|
* draw_uniform per-draw vec3 offset uniform + draw (chunk sections)
|
||||||
|
* draw_multi_vao per-draw VAO/VBO switch + draw (per-section buffers)
|
||||||
|
* tex_pingpong per-draw texture bind churn on one unit
|
||||||
|
* program_pingpong alternate two programs + mat4 upload (chunk<->entity)
|
||||||
|
* chunk_upload glBufferData(NULL) orphan + glBufferSubData + draw
|
||||||
|
* atlas_sprite N 16x16 glTexSubImage2D into a 1024x512 atlas + draw
|
||||||
|
* lightmap full 16x16 lightmap respecify per frame + draw
|
||||||
|
* scene_mix composite frame built from the knobs below
|
||||||
|
*
|
||||||
|
* Output: one CSV line per case:
|
||||||
|
* case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps
|
||||||
|
*/
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* ---- EGL constants ---- */
|
||||||
|
typedef void* EGLDisplay;
|
||||||
|
typedef void* EGLConfig;
|
||||||
|
typedef void* EGLContext;
|
||||||
|
typedef void* EGLSurface;
|
||||||
|
typedef int EGLint;
|
||||||
|
typedef unsigned int EGLBoolean;
|
||||||
|
typedef unsigned int EGLenum;
|
||||||
|
#define EGL_DEFAULT_DISPLAY ((void*)0)
|
||||||
|
#define EGL_NO_CONTEXT ((EGLContext)0)
|
||||||
|
#define EGL_NO_SURFACE ((EGLSurface)0)
|
||||||
|
#define EGL_FALSE 0
|
||||||
|
#define EGL_SURFACE_TYPE 0x3033
|
||||||
|
#define EGL_PBUFFER_BIT 0x0001
|
||||||
|
#define EGL_RENDERABLE_TYPE 0x3040
|
||||||
|
#define EGL_OPENGL_BIT 0x0008
|
||||||
|
#define EGL_RED_SIZE 0x3024
|
||||||
|
#define EGL_GREEN_SIZE 0x3023
|
||||||
|
#define EGL_BLUE_SIZE 0x3022
|
||||||
|
#define EGL_DEPTH_SIZE 0x3025
|
||||||
|
#define EGL_WIDTH 0x3057
|
||||||
|
#define EGL_HEIGHT 0x3056
|
||||||
|
#define EGL_NONE 0x3038
|
||||||
|
#define EGL_OPENGL_API 0x30A2
|
||||||
|
#define EGL_OPENGL_ES_API 0x30A0
|
||||||
|
#define EGL_OPENGL_ES3_BIT 0x0040
|
||||||
|
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
|
||||||
|
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
|
||||||
|
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||||
|
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||||
|
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||||
|
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
|
||||||
|
|
||||||
|
/* ---- GL constants ---- */
|
||||||
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||||
|
#define GL_DEPTH_BUFFER_BIT 0x00000100
|
||||||
|
#define GL_TRIANGLES 0x0004
|
||||||
|
#define GL_UNSIGNED_INT 0x1405
|
||||||
|
#define GL_SHORT 0x1402
|
||||||
|
#define GL_FLOAT 0x1406
|
||||||
|
#define GL_UNSIGNED_BYTE 0x1401
|
||||||
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
|
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
|
||||||
|
#define GL_STATIC_DRAW 0x88E4
|
||||||
|
#define GL_TEXTURE_2D 0x0DE1
|
||||||
|
#define GL_TEXTURE0 0x84C0
|
||||||
|
#define GL_RGBA 0x1908
|
||||||
|
#define GL_RGBA8 0x8058
|
||||||
|
#define GL_DEPTH_COMPONENT24 0x81A6
|
||||||
|
#define GL_TEXTURE_MIN_FILTER 0x2801
|
||||||
|
#define GL_TEXTURE_MAG_FILTER 0x2800
|
||||||
|
#define GL_NEAREST 0x2600
|
||||||
|
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
||||||
|
#define GL_DEPTH_TEST 0x0B71
|
||||||
|
#define GL_BLEND 0x0BE2
|
||||||
|
#define GL_SRC_ALPHA 0x0302
|
||||||
|
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
|
||||||
|
#define GL_ONE 1
|
||||||
|
#define GL_ZERO 0
|
||||||
|
#define GL_VERTEX_SHADER 0x8B31
|
||||||
|
#define GL_FRAGMENT_SHADER 0x8B30
|
||||||
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
|
#define GL_LINK_STATUS 0x8B82
|
||||||
|
#define GL_VERSION 0x1F02
|
||||||
|
#define GL_RENDERER 0x1F01
|
||||||
|
#define GL_NO_ERROR 0
|
||||||
|
#define GL_FRAMEBUFFER 0x8D40
|
||||||
|
#define GL_RENDERBUFFER 0x8D41
|
||||||
|
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
||||||
|
#define GL_DEPTH_ATTACHMENT 0x8D00
|
||||||
|
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||||||
|
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||||
|
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
||||||
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
|
#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
|
||||||
|
#define GL_DYNAMIC_DRAW 0x88E8
|
||||||
|
#define GL_STREAM_DRAW 0x88E0
|
||||||
|
#define GL_UNPACK_ALIGNMENT 0x0CF5
|
||||||
|
#define GL_UNPACK_ROW_LENGTH 0x0CF2
|
||||||
|
#define GL_UNPACK_SKIP_ROWS 0x0CF3
|
||||||
|
#define GL_UNPACK_SKIP_PIXELS 0x0CF4
|
||||||
|
#define GL_TEXTURE_WRAP_S 0x2802
|
||||||
|
#define GL_TEXTURE_WRAP_T 0x2803
|
||||||
|
#define GL_CLAMP_TO_EDGE 0x812F
|
||||||
|
#define GL_REPEAT 0x2901
|
||||||
|
|
||||||
|
typedef unsigned int GLuint;
|
||||||
|
typedef int GLint;
|
||||||
|
typedef int GLsizei;
|
||||||
|
typedef unsigned int GLenum;
|
||||||
|
typedef char GLchar;
|
||||||
|
typedef unsigned char GLboolean;
|
||||||
|
typedef long GLsizeiptr;
|
||||||
|
typedef long GLintptr;
|
||||||
|
|
||||||
|
/* ---- resolved entry points ---- */
|
||||||
|
static void* (*g_eglGetProcAddress)(const char*);
|
||||||
|
static void* g_provider;
|
||||||
|
|
||||||
|
#define GLF(ret, name, args) static ret(*name) args;
|
||||||
|
GLF(void, glClear, (unsigned))
|
||||||
|
GLF(void, glClearColor, (float, float, float, float))
|
||||||
|
GLF(void, glEnable, (GLenum))
|
||||||
|
GLF(void, glDisable, (GLenum))
|
||||||
|
GLF(void, glBlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum))
|
||||||
|
GLF(void, glDrawBuffers, (GLsizei, const GLenum*))
|
||||||
|
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
|
||||||
|
GLF(const unsigned char*, glGetString, (GLenum))
|
||||||
|
GLF(GLenum, glGetError, (void))
|
||||||
|
GLF(void, glFinish, (void))
|
||||||
|
GLF(void, glFlush, (void))
|
||||||
|
GLF(void, glGenBuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindBuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum))
|
||||||
|
GLF(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*))
|
||||||
|
GLF(void, glGenVertexArrays, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindVertexArray, (GLuint))
|
||||||
|
GLF(void, glEnableVertexAttribArray, (GLuint))
|
||||||
|
GLF(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*))
|
||||||
|
GLF(void, glGenTextures, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindTexture, (GLenum, GLuint))
|
||||||
|
GLF(void, glActiveTexture, (GLenum))
|
||||||
|
GLF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*))
|
||||||
|
GLF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))
|
||||||
|
GLF(void, glTexParameteri, (GLenum, GLenum, GLint))
|
||||||
|
GLF(void, glPixelStorei, (GLenum, GLint))
|
||||||
|
GLF(void, glGetIntegerv, (GLenum, GLint*))
|
||||||
|
GLF(void, glGenerateMipmap, (GLenum))
|
||||||
|
GLF(GLuint, glCreateShader, (GLenum))
|
||||||
|
GLF(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*))
|
||||||
|
GLF(void, glCompileShader, (GLuint))
|
||||||
|
GLF(void, glGetShaderiv, (GLuint, GLenum, GLint*))
|
||||||
|
GLF(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*))
|
||||||
|
GLF(GLuint, glCreateProgram, (void))
|
||||||
|
GLF(void, glAttachShader, (GLuint, GLuint))
|
||||||
|
GLF(void, glLinkProgram, (GLuint))
|
||||||
|
GLF(void, glGetProgramiv, (GLuint, GLenum, GLint*))
|
||||||
|
GLF(void, glUseProgram, (GLuint))
|
||||||
|
GLF(GLint, glGetUniformLocation, (GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniform1i, (GLint, GLint))
|
||||||
|
GLF(void, glUniform3f, (GLint, float, float, float))
|
||||||
|
GLF(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const float*))
|
||||||
|
GLF(void, glDrawElements, (GLenum, GLsizei, GLenum, const void*))
|
||||||
|
GLF(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniform3fv, (GLint, GLsizei, const float*))
|
||||||
|
GLF(void, glDrawArrays, (GLenum, GLint, GLsizei))
|
||||||
|
GLF(void, glDrawElementsBaseVertex, (GLenum, GLsizei, GLenum, const void*, GLint))
|
||||||
|
GLF(void, glMultiDrawElementsBaseVertex,
|
||||||
|
(GLenum, const GLsizei*, GLenum, const void* const*, GLsizei, const GLint*))
|
||||||
|
GLF(void, glBindBufferRange, (GLenum, GLuint, GLuint, GLintptr, GLsizeiptr))
|
||||||
|
GLF(void, glBindBufferBase, (GLenum, GLuint, GLuint))
|
||||||
|
GLF(GLuint, glGetUniformBlockIndex, (GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniformBlockBinding, (GLuint, GLuint, GLuint))
|
||||||
|
GLF(void, glGenSamplers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindSampler, (GLuint, GLuint))
|
||||||
|
GLF(void, glSamplerParameteri, (GLuint, GLenum, GLint))
|
||||||
|
GLF(void, glGenFramebuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindFramebuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glGenRenderbuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindRenderbuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei))
|
||||||
|
GLF(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint))
|
||||||
|
GLF(GLenum, glCheckFramebufferStatus, (GLenum))
|
||||||
|
GLF(void*, glFenceSync, (GLenum, unsigned))
|
||||||
|
GLF(GLenum, glClientWaitSync, (void*, unsigned, unsigned long long))
|
||||||
|
GLF(void, glDeleteSync, (void*))
|
||||||
|
|
||||||
|
static uint64_t now_ns(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_u64(const void* a, const void* b) {
|
||||||
|
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
|
||||||
|
return x < y ? -1 : x > y;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Scene, cases and the case table live next door so the Android plugin's
|
||||||
|
* in-process benchmark runs byte-identical bodies. */
|
||||||
|
static void bench_gl_failed(const char* what, const char* detail) {
|
||||||
|
fprintf(stderr, "FAIL: %s %s\n", what, detail ? detail : "");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GLES has glDrawElementsBaseVertex (3.2 core) but no multi-draw form of it, so
|
||||||
|
* against a native mobile driver the multi-draw case issues the same sub-draws
|
||||||
|
* one at a time - which is what the extension folds up, and what an application
|
||||||
|
* without it would have to write. Desktop GL and MobileGL take the real call. */
|
||||||
|
static void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type,
|
||||||
|
const void* const* offsets, GLsizei drawCount,
|
||||||
|
const GLint* baseVertices) {
|
||||||
|
if (glMultiDrawElementsBaseVertex) {
|
||||||
|
glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < drawCount; ++i) {
|
||||||
|
glDrawElementsBaseVertex(mode, counts[i], type, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "DriverBenchCases.inc"
|
||||||
|
|
||||||
|
/* ---- bench driver: fence-paced frames on the offscreen FBO ----------------
|
||||||
|
* Frames are closed with a real fence wait, not glFinish: MobileGL implements
|
||||||
|
* glFinish and glFlush as no-ops (MG_Impl/GLImpl/Exporting/Definitions.cpp),
|
||||||
|
* so a glFinish-paced loop would time only the CPU-side submit on a MobileGL
|
||||||
|
* backend while timing submit-plus-GPU on the native driver - the two numbers
|
||||||
|
* would not describe the same work. A sync object is honoured by every stack
|
||||||
|
* measured here.
|
||||||
|
*/
|
||||||
|
typedef void (*case_fn)(int frame, long a, long b);
|
||||||
|
static int g_warmup = 30, g_frames = 120;
|
||||||
|
|
||||||
|
static void end_frame_wait(void) {
|
||||||
|
if (glFenceSync && glClientWaitSync && glDeleteSync) {
|
||||||
|
void* sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
if (sync) {
|
||||||
|
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
|
||||||
|
glDeleteSync(sync);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_case(const char* name, case_fn body, long a, long b, long opsPerFrame) {
|
||||||
|
static uint64_t samples[4096];
|
||||||
|
if (g_frames > 4096) g_frames = 4096;
|
||||||
|
end_frame_wait();
|
||||||
|
for (int i = 0; i < g_warmup; ++i) {
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
body(i, a, b);
|
||||||
|
end_frame_wait();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < g_frames; ++i) {
|
||||||
|
uint64_t t0 = now_ns();
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
body(i, a, b);
|
||||||
|
end_frame_wait();
|
||||||
|
samples[i] = now_ns() - t0;
|
||||||
|
}
|
||||||
|
qsort(samples, g_frames, sizeof(uint64_t), cmp_u64);
|
||||||
|
uint64_t med = samples[g_frames / 2];
|
||||||
|
double frameMs = med / 1e6;
|
||||||
|
double nsPerOp = opsPerFrame > 0 ? (double)med / (double)opsPerFrame : 0.0;
|
||||||
|
printf("%s,%d,%ld,%.3f,%.1f,%.1f\n", name, g_frames, opsPerFrame, frameMs, nsPerOp,
|
||||||
|
1e9 / (double)med);
|
||||||
|
fflush(stdout);
|
||||||
|
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A display that needs no window system. eglGetPlatformDisplay is EGL 1.5
|
||||||
|
* core and eglGetPlatformDisplayEXT is the EGL_EXT_platform_base spelling
|
||||||
|
* older loaders ship; both are client entry points, so they resolve before
|
||||||
|
* any display exists. Only the attribute-list types differ between the two
|
||||||
|
* and this passes none, so one cast covers both. */
|
||||||
|
static EGLDisplay surfaceless_display(void) {
|
||||||
|
void* fn = dlsym(g_provider, "eglGetPlatformDisplay");
|
||||||
|
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplay");
|
||||||
|
if (!fn) fn = dlsym(g_provider, "eglGetPlatformDisplayEXT");
|
||||||
|
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplayEXT");
|
||||||
|
if (!fn) return NULL;
|
||||||
|
return ((EGLDisplay(*)(EGLenum, void*, const void*))fn)(EGL_PLATFORM_SURFACELESS_MESA,
|
||||||
|
EGL_DEFAULT_DISPLAY, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
|
||||||
|
static int boot_egl(void) {
|
||||||
|
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
|
||||||
|
if (!libpath) libpath = "libEGL.so.1";
|
||||||
|
g_provider = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
|
||||||
|
if (!g_provider) {
|
||||||
|
fprintf(stderr, "FAIL: dlopen %s: %s\n", libpath, dlerror());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#define ESYM(name) \
|
||||||
|
void* p_##name = dlsym(g_provider, #name); \
|
||||||
|
if (!p_##name) { fprintf(stderr, "FAIL: dlsym %s\n", #name); return 1; }
|
||||||
|
ESYM(eglGetDisplay)
|
||||||
|
ESYM(eglInitialize)
|
||||||
|
ESYM(eglChooseConfig)
|
||||||
|
ESYM(eglBindAPI)
|
||||||
|
ESYM(eglCreateContext)
|
||||||
|
ESYM(eglCreatePbufferSurface)
|
||||||
|
ESYM(eglMakeCurrent)
|
||||||
|
ESYM(eglGetProcAddress)
|
||||||
|
ESYM(eglGetError)
|
||||||
|
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
|
||||||
|
|
||||||
|
EGLint (*getError)(void) = (EGLint(*)(void))p_eglGetError;
|
||||||
|
EGLBoolean (*initialize)(EGLDisplay, EGLint*, EGLint*) =
|
||||||
|
(EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize;
|
||||||
|
|
||||||
|
/* The default display first: it is the one a windowed app would get, and
|
||||||
|
* on a desktop it is the one that reaches the real GPU - which is the
|
||||||
|
* driver this bench exists to measure. It does need a window system,
|
||||||
|
* though; Mesa's default platform is X11, so with no $DISPLAY (CI, a
|
||||||
|
* build server, ssh without forwarding) eglInitialize fails. Fall back to
|
||||||
|
* EGL_MESA_platform_surfaceless rather than give up: every case draws into
|
||||||
|
* the FBO built by build_resources(), so no window is needed for any of
|
||||||
|
* the work being timed. */
|
||||||
|
EGLint maj = 0, min = 0;
|
||||||
|
const char* how = "default display";
|
||||||
|
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (!dpy || !initialize(dpy, &maj, &min)) {
|
||||||
|
dpy = surfaceless_display();
|
||||||
|
how = "surfaceless display";
|
||||||
|
if (!dpy || !initialize(dpy, &maj, &min)) {
|
||||||
|
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "EGL %d.%d via %s (%s)\n", maj, min, libpath, how);
|
||||||
|
|
||||||
|
// Desktop GL first (that is what MobileGL exposes and what the cases are
|
||||||
|
// written against), GLES 3 second so the same binary can measure a device's
|
||||||
|
// native driver as the baseline. The .inc picks ESSL shader sources when the
|
||||||
|
// context turns out to be ES.
|
||||||
|
EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*) =
|
||||||
|
(EGLBoolean(*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*))p_eglChooseConfig;
|
||||||
|
EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) =
|
||||||
|
(EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext;
|
||||||
|
EGLBoolean (*bindApi)(EGLenum) = (EGLBoolean(*)(EGLenum))p_eglBindAPI;
|
||||||
|
|
||||||
|
EGLConfig cfg = NULL;
|
||||||
|
EGLint ncfg = 0;
|
||||||
|
EGLContext ctx = EGL_NO_CONTEXT;
|
||||||
|
|
||||||
|
if (bindApi(EGL_OPENGL_API)) {
|
||||||
|
const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
|
||||||
|
if (chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) && ncfg >= 1) {
|
||||||
|
const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||||
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||||
|
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
|
||||||
|
if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
|
if (!bindApi(EGL_OPENGL_ES_API)) {
|
||||||
|
fprintf(stderr, "FAIL: neither OpenGL nor OpenGL ES is bindable on this provider\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const EGLint esCfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||||
|
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
|
||||||
|
ncfg = 0;
|
||||||
|
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
|
// EGL_SURFACE_TYPE 0 matches any config: a stack that offers no
|
||||||
|
// pbuffer at all is still usable through the surfaceless context
|
||||||
|
// path below.
|
||||||
|
const EGLint relaxed[] = {EGL_SURFACE_TYPE, 0, EGL_RED_SIZE, 8, EGL_NONE};
|
||||||
|
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
|
fprintf(stderr, "FAIL: eglChooseConfig\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EGLint esCtxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||||
|
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
|
||||||
|
}
|
||||||
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
|
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The pbuffer only exists to have something to make current - nothing is
|
||||||
|
* ever drawn to it. Where there is no pbuffer config, EGL_NO_SURFACE is
|
||||||
|
* exactly what EGL_KHR_surfaceless_context takes, so the same call covers
|
||||||
|
* both. */
|
||||||
|
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||||
|
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
|
||||||
|
dpy, cfg, pbAttribs);
|
||||||
|
if (surf == EGL_NO_SURFACE)
|
||||||
|
fprintf(stderr, "no pbuffer (0x%x), using a surfaceless context\n", getError());
|
||||||
|
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
|
||||||
|
surf, ctx)) {
|
||||||
|
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Core GL entry points: eglGetProcAddress first (EGL 1.5 serves core
|
||||||
|
* functions), provider dlsym as fallback (both glvnd and MobileGL export
|
||||||
|
* the gl* symbols directly). */
|
||||||
|
#define RESOLVE(name) \
|
||||||
|
do { \
|
||||||
|
*(void**)&name = g_eglGetProcAddress(#name); \
|
||||||
|
if (!name) *(void**)&name = dlsym(g_provider, #name); \
|
||||||
|
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
|
||||||
|
} while (0)
|
||||||
|
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
|
||||||
|
RESOLVE(glDisable); RESOLVE(glBlendFuncSeparate); RESOLVE(glDrawBuffers);
|
||||||
|
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
|
||||||
|
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
|
||||||
|
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
|
||||||
|
RESOLVE(glVertexAttribPointer); RESOLVE(glGenTextures); RESOLVE(glBindTexture);
|
||||||
|
RESOLVE(glActiveTexture); RESOLVE(glTexImage2D); RESOLVE(glTexSubImage2D);
|
||||||
|
RESOLVE(glTexParameteri); RESOLVE(glGenerateMipmap); RESOLVE(glCreateShader);
|
||||||
|
RESOLVE(glPixelStorei); RESOLVE(glGetIntegerv);
|
||||||
|
RESOLVE(glShaderSource); RESOLVE(glCompileShader); RESOLVE(glGetShaderiv);
|
||||||
|
RESOLVE(glGetShaderInfoLog); RESOLVE(glCreateProgram); RESOLVE(glAttachShader);
|
||||||
|
RESOLVE(glLinkProgram); RESOLVE(glGetProgramiv); RESOLVE(glUseProgram);
|
||||||
|
RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f);
|
||||||
|
RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation);
|
||||||
|
RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex);
|
||||||
|
RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase);
|
||||||
|
RESOLVE(glGetUniformBlockIndex); RESOLVE(glUniformBlockBinding);
|
||||||
|
RESOLVE(glGenSamplers); RESOLVE(glBindSampler); RESOLVE(glSamplerParameteri);
|
||||||
|
RESOLVE(glGenFramebuffers); RESOLVE(glBindFramebuffer); RESOLVE(glGenRenderbuffers);
|
||||||
|
RESOLVE(glBindRenderbuffer); RESOLVE(glRenderbufferStorage); RESOLVE(glFramebufferRenderbuffer);
|
||||||
|
RESOLVE(glCheckFramebufferStatus);
|
||||||
|
// Optional: end_frame_wait() falls back to glFinish when a stack has no
|
||||||
|
// sync objects, so resolve without failing the run.
|
||||||
|
*(void**)&glFenceSync = g_eglGetProcAddress("glFenceSync");
|
||||||
|
if (!glFenceSync) *(void**)&glFenceSync = dlsym(g_provider, "glFenceSync");
|
||||||
|
*(void**)&glClientWaitSync = g_eglGetProcAddress("glClientWaitSync");
|
||||||
|
if (!glClientWaitSync) *(void**)&glClientWaitSync = dlsym(g_provider, "glClientWaitSync");
|
||||||
|
*(void**)&glDeleteSync = g_eglGetProcAddress("glDeleteSync");
|
||||||
|
if (!glDeleteSync) *(void**)&glDeleteSync = dlsym(g_provider, "glDeleteSync");
|
||||||
|
// Desktop-only: GLES 3.2 has DrawElementsBaseVertex but no multi-draw form,
|
||||||
|
// so bench_multi_draw_elements_base_vertex() emulates it when this is null.
|
||||||
|
*(void**)&glMultiDrawElementsBaseVertex = g_eglGetProcAddress("glMultiDrawElementsBaseVertex");
|
||||||
|
if (!glMultiDrawElementsBaseVertex)
|
||||||
|
*(void**)&glMultiDrawElementsBaseVertex = dlsym(g_provider, "glMultiDrawElementsBaseVertex");
|
||||||
|
|
||||||
|
fprintf(stderr, "renderer: %s\n", glGetString(GL_RENDERER));
|
||||||
|
fprintf(stderr, "version: %s\n", glGetString(GL_VERSION));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
long draws = 2048;
|
||||||
|
if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS"));
|
||||||
|
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
||||||
|
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||||
|
|
||||||
|
if (boot_egl()) return 1;
|
||||||
|
build_resources();
|
||||||
|
|
||||||
|
printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n");
|
||||||
|
for (int i = 0; i < kBenchCaseCount; ++i) {
|
||||||
|
const BenchCaseDesc* c = &kBenchCases[i];
|
||||||
|
if (argc > 1) {
|
||||||
|
int wanted = 0;
|
||||||
|
for (int j = 1; j < argc; ++j)
|
||||||
|
if (strcmp(argv[j], c->name) == 0) wanted = 1;
|
||||||
|
if (!wanted) continue;
|
||||||
|
}
|
||||||
|
// The generic cases scale with DRIVERBENCH_DRAWS; the mc_* rates are
|
||||||
|
// measured and must not move, or the numbers stop being comparable.
|
||||||
|
long a = c->a, ops = c->opsPerFrame;
|
||||||
|
if (strncmp(c->name, "mc_", 3) != 0 && a > 100) {
|
||||||
|
a = draws * a / 2048;
|
||||||
|
ops = c->opsPerFrame * draws / 2048;
|
||||||
|
}
|
||||||
|
run_case(c->name, c->fn, a, c->b, ops);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc
|
||||||
|
* Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
* Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
* https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
* SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
* End of Source File Header
|
||||||
|
*
|
||||||
|
* The benchmark scene and its cases, with no harness and no GL loader: the
|
||||||
|
* includer supplies both. DriverBench.c drives it through function pointers
|
||||||
|
* resolved from one EGL provider; MG_Util/SelfTest/DriverBenchJni.cpp drives
|
||||||
|
* it through MobileGL's own frontend entry points inside the Android plugin.
|
||||||
|
* Sharing the bodies is the point - a number from the phone and a number from
|
||||||
|
* the desktop have to describe the same work.
|
||||||
|
*
|
||||||
|
* The includer must have declared, before including this file: the GL types
|
||||||
|
* and enums used below, and callable gl* entry points with the standard
|
||||||
|
* signatures. bench_gl_failed() is called (and must be defined) when shader
|
||||||
|
* compilation or linking fails, so a caller can report the failure instead of
|
||||||
|
* dying inside a benchmark.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---- shared scene resources (Minecraft-shaped) ---- */
|
||||||
|
#define MAX_SECTIONS 512
|
||||||
|
static GLuint g_progChunk, g_progEntity;
|
||||||
|
static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity;
|
||||||
|
static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS];
|
||||||
|
static GLuint g_sharedIbo;
|
||||||
|
static GLuint g_texAtlas, g_texLight, g_texEntity;
|
||||||
|
static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */
|
||||||
|
static unsigned char* g_scratch;
|
||||||
|
/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */
|
||||||
|
static GLuint g_uboRing;
|
||||||
|
static GLint g_uboAlign = 256;
|
||||||
|
static size_t g_uboSlot = 256;
|
||||||
|
static GLuint g_sampler;
|
||||||
|
/* Two small offscreen targets for the 26.2-style render-pass churn case. */
|
||||||
|
static GLuint g_passFbo[2];
|
||||||
|
static GLuint g_passColor[2];
|
||||||
|
static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f};
|
||||||
|
|
||||||
|
/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */
|
||||||
|
#define VERT_STRIDE 32
|
||||||
|
static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) {
|
||||||
|
for (int q = 0; q < quads * 4; ++q) {
|
||||||
|
float* f = (float*)(dst + q * VERT_STRIDE);
|
||||||
|
unsigned r = seed = seed * 1664525u + 1013904223u;
|
||||||
|
f[0] = (float)(q & 31) * 8.0f + (float)(r & 7);
|
||||||
|
f[1] = (float)((q >> 5) & 31) * 8.0f;
|
||||||
|
f[2] = (float)(q % 7) * 0.1f;
|
||||||
|
dst[q * VERT_STRIDE + 12] = (unsigned char)r;
|
||||||
|
dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8);
|
||||||
|
dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16);
|
||||||
|
dst[q * VERT_STRIDE + 15] = 255;
|
||||||
|
f[4] = (float)(r & 1023) / 1024.0f;
|
||||||
|
f[5] = (float)((r >> 10) & 511) / 512.0f;
|
||||||
|
((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4;
|
||||||
|
((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint make_shader(GLenum kind, const char* src) {
|
||||||
|
GLuint sh = glCreateShader(kind);
|
||||||
|
glShaderSource(sh, 1, &src, NULL);
|
||||||
|
glCompileShader(sh);
|
||||||
|
GLint ok = 0;
|
||||||
|
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
char log[1024];
|
||||||
|
glGetShaderInfoLog(sh, sizeof log, NULL, log);
|
||||||
|
bench_gl_failed("shader compile", log);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint make_program(const char* vs_src, const char* fs_src) {
|
||||||
|
GLuint prog = glCreateProgram();
|
||||||
|
glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src));
|
||||||
|
glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src));
|
||||||
|
glBindAttribLocation(prog, 0, "aPos");
|
||||||
|
glBindAttribLocation(prog, 1, "aColor");
|
||||||
|
glBindAttribLocation(prog, 2, "aUv");
|
||||||
|
glBindAttribLocation(prog, 3, "aLight");
|
||||||
|
glLinkProgram(prog);
|
||||||
|
GLint ok = 0;
|
||||||
|
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
bench_gl_failed("program link", "");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kChunkVs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||||
|
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||||
|
static const char* kChunkFs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||||
|
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||||
|
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||||
|
static const char* kEntityVs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||||
|
static const char* kEntityFs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||||
|
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||||
|
|
||||||
|
// ESSL 3.20 twins of the four shaders above. The bodies are identical; only the
|
||||||
|
// version line and the precision qualifiers differ, so the two paths compile the
|
||||||
|
// same work. Needed because this bench also runs against a device's native GLES
|
||||||
|
// driver as the baseline MobileGL is measured against, and that driver rejects
|
||||||
|
// desktop GLSL - while MobileGL is fed desktop GLSL on purpose, since translating
|
||||||
|
// it is the thing under test.
|
||||||
|
static const char* kChunkVsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||||
|
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||||
|
static const char* kChunkFsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision mediump float;\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||||
|
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||||
|
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||||
|
static const char* kEntityVsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||||
|
static const char* kEntityFsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision mediump float;\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||||
|
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||||
|
|
||||||
|
// True once build_resources() has seen a GL_VERSION beginning with "OpenGL ES".
|
||||||
|
static int g_isGlesContext = 0;
|
||||||
|
|
||||||
|
static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glEnableVertexAttribArray(2);
|
||||||
|
glEnableVertexAttribArray(3);
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0);
|
||||||
|
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12);
|
||||||
|
glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16);
|
||||||
|
glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint g_mainFbo;
|
||||||
|
|
||||||
|
static void build_resources(void) {
|
||||||
|
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
|
||||||
|
GLuint fbo, rboColor, rboDepth;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
g_mainFbo = fbo;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glGenRenderbuffers(1, &rboColor);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor);
|
||||||
|
glGenRenderbuffers(1, &rboDepth);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
bench_gl_failed("FBO incomplete", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* versionString = (const char*)glGetString(GL_VERSION);
|
||||||
|
g_isGlesContext = versionString != NULL && strncmp(versionString, "OpenGL ES", 9) == 0;
|
||||||
|
g_progChunk = g_isGlesContext ? make_program(kChunkVsEs, kChunkFsEs) : make_program(kChunkVs, kChunkFs);
|
||||||
|
g_progEntity = g_isGlesContext ? make_program(kEntityVsEs, kEntityFsEs) : make_program(kEntityVs, kEntityFs);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp");
|
||||||
|
g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset");
|
||||||
|
glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0);
|
||||||
|
glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2);
|
||||||
|
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp");
|
||||||
|
glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
|
||||||
|
/* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */
|
||||||
|
int maxQuads = 4096;
|
||||||
|
unsigned* idx = (unsigned*)malloc((size_t)maxQuads * 6 * 4);
|
||||||
|
for (int q = 0; q < maxQuads; ++q) {
|
||||||
|
unsigned base = q * 4;
|
||||||
|
unsigned* p = idx + q * 6;
|
||||||
|
p[0] = base; p[1] = base + 1; p[2] = base + 2;
|
||||||
|
p[3] = base + 2; p[4] = base + 3; p[5] = base;
|
||||||
|
}
|
||||||
|
glGenBuffers(1, &g_sharedIbo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW);
|
||||||
|
free(idx);
|
||||||
|
|
||||||
|
g_scratch = (unsigned char*)malloc(4 * 1024 * 1024);
|
||||||
|
memset(g_scratch, 0x5a, 4 * 1024 * 1024);
|
||||||
|
|
||||||
|
glGenVertexArrays(MAX_SECTIONS, g_vao);
|
||||||
|
glGenBuffers(MAX_SECTIONS, g_vbo);
|
||||||
|
int bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
for (int i = 0; i < MAX_SECTIONS; ++i) {
|
||||||
|
fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW);
|
||||||
|
setup_vao(g_vao[i], g_vbo[i], g_sharedIbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texAtlas);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texLight);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texEntity);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
|
||||||
|
// Uniform ring the 26.2-style case sub-ranges into, sized like a real
|
||||||
|
// frame's worth of per-draw uniform slots.
|
||||||
|
GLint align = 256;
|
||||||
|
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align);
|
||||||
|
g_uboAlign = align > 0 ? align : 256;
|
||||||
|
g_uboSlot = (size_t)g_uboAlign;
|
||||||
|
glGenBuffers(1, &g_uboRing);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
glGenFramebuffers(1, &g_passFbo[i]);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i]);
|
||||||
|
glGenRenderbuffers(1, &g_passColor[i]);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, g_passColor[i]);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 256, 256);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, g_passColor[i]);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
bench_gl_failed("pass FBO incomplete", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* back to the main offscreen target the harness set up */
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
|
||||||
|
|
||||||
|
glGenSamplers(1, &g_sampler);
|
||||||
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glClearColor(0.3f, 0.5f, 0.9f, 1.0f);
|
||||||
|
glViewport(0, 0, 1280, 720);
|
||||||
|
const GLenum setupError = glGetError();
|
||||||
|
if (setupError != GL_NO_ERROR) {
|
||||||
|
char message[64];
|
||||||
|
snprintf(message, sizeof message, "0x%04x", setupError);
|
||||||
|
bench_gl_failed("GL error during resource setup", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_tiny(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_uniform(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_multi_vao(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_tex_pingpong(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_program_pingpong(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i & 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
} else {
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = uploads per frame, b = bytes per upload (0 => section size) */
|
||||||
|
static void case_chunk_upload(int frame, long a, long b) {
|
||||||
|
if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
int slot = (int)(((long)frame * a + i) % MAX_SECTIONS);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch);
|
||||||
|
glBindVertexArray(g_vao[slot]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = sprite updates per frame */
|
||||||
|
static void case_atlas_sprite(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = lightmap updates (+draw) per frame */
|
||||||
|
static void case_lightmap(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace
|
||||||
|
* analysis: chunk draws with per-draw offset uniform across sections, 10%
|
||||||
|
* entity-style program flips, per-frame lightmap + sprite updates, b chunk
|
||||||
|
* re-uploads. */
|
||||||
|
static long g_mixSprites = 8;
|
||||||
|
static void case_scene_mix(int frame, long a, long b) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < g_mixSprites; ++i) {
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
for (long i = 0; i < b; ++i) {
|
||||||
|
int slot = (int)(((long)frame * b + i) % MAX_SECTIONS);
|
||||||
|
long bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch);
|
||||||
|
}
|
||||||
|
long entityEvery = 10;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i % entityEvery == entityEvery - 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
} else {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Trace-derived cases -------------------------------------------------
|
||||||
|
* Per-frame call mixes measured from the three captured Minecraft traces
|
||||||
|
* (render distance 32, 1280x720, hovering in-world). Each case reproduces one
|
||||||
|
* renderer's dominant per-draw sequence at its measured rate, so the number a
|
||||||
|
* backend posts here is directly comparable to what that game version asks of
|
||||||
|
* the driver every frame.
|
||||||
|
*
|
||||||
|
* vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray,
|
||||||
|
* 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei,
|
||||||
|
* 247 glTexParameteri), 23 glBufferData per frame
|
||||||
|
* fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray,
|
||||||
|
* 132 glUniform3f, 32 glBufferData per frame
|
||||||
|
* 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by
|
||||||
|
* glBindBufferRange + glBindBuffer (3639/3412 per frame)
|
||||||
|
*/
|
||||||
|
/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */
|
||||||
|
static void case_mc_vanilla_draw(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
float offset[3];
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
offset[0] = (float)(i & 15);
|
||||||
|
offset[1] = (float)((i >> 4) & 15);
|
||||||
|
offset[2] = 0.0f;
|
||||||
|
glUniform3fv(g_uOffsetChunk, 1, offset);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sodium: one multi-draw covers many chunk sections out of a shared buffer.
|
||||||
|
* a = multi-draws per frame, b = sub-draws inside each. */
|
||||||
|
static void case_mc_sodium_multidraw(int frame, long a, long b) {
|
||||||
|
(void)frame;
|
||||||
|
enum { kMaxSub = 64 };
|
||||||
|
if (b <= 0 || b > kMaxSub) b = 32;
|
||||||
|
GLsizei counts[kMaxSub];
|
||||||
|
const void* offsets[kMaxSub];
|
||||||
|
GLint baseVertices[kMaxSub];
|
||||||
|
for (long s = 0; s < b; ++s) {
|
||||||
|
counts[s] = (GLsizei)(g_quadsPerSection * 6 / b);
|
||||||
|
offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4);
|
||||||
|
baseVertices[s] = 0;
|
||||||
|
}
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
// Routed through the includer: GLES has no multi-draw-with-base-vertex, so
|
||||||
|
// a native-driver harness emulates it with the loop the extension folds up.
|
||||||
|
bench_multi_draw_elements_base_vertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets,
|
||||||
|
(GLsizei)b, baseVertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring.
|
||||||
|
* a = draws per frame. */
|
||||||
|
static void case_mc_ubo_range(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
const size_t slots = (4u * 1024u * 1024u) / g_uboSlot;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
const size_t slot = (size_t)(((long)frame * a + i) % (long)slots);
|
||||||
|
glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot),
|
||||||
|
(GLsizeiptr)g_uboSlot);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store
|
||||||
|
* and filter state Blaze3D re-sets around it. a = uploads per frame. */
|
||||||
|
static void case_mc_tex_stream(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||||
|
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */
|
||||||
|
static void case_mc_uniform_lookup(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"};
|
||||||
|
volatile GLint sink = 0;
|
||||||
|
for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]);
|
||||||
|
(void)sink;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */
|
||||||
|
static void case_mc_sampler_churn(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3));
|
||||||
|
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||||
|
glBindSampler((GLuint)(i & 3), g_sampler);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 26.2 switches render targets constantly: 132 glBindFramebuffer and 198
|
||||||
|
* glDrawBuffers per frame. Pass switching is where a Vulkan backend pays for
|
||||||
|
* render-pass breaks, so this case is the one to watch on Magma. a = passes. */
|
||||||
|
static void case_mc_pass_switch(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
static const GLenum kColor0[1] = {GL_COLOR_ATTACHMENT0};
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i & 1]);
|
||||||
|
glDrawBuffers(1, kColor0);
|
||||||
|
glViewport(0, 0, 256, 256);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
|
||||||
|
glViewport(0, 0, 1280, 720);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blaze3D toggles blend around batches: 46 glEnable/glDisable pairs and 28
|
||||||
|
* glBlendFuncSeparate per vanilla frame. a = toggle pairs. */
|
||||||
|
static void case_mc_state_toggle(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2 re-sets texture parameters relentlessly - 612 glTexParameteri per frame,
|
||||||
|
* almost always to the value already in place. Measures redundant-param
|
||||||
|
* filtering. a = parameter writes. */
|
||||||
|
static void case_mc_tex_param(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; i += 4) {
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sodium switches programs mid-frame far more than vanilla: 62 glUseProgram and
|
||||||
|
* 60 mat4 uploads per frame. a = program switches. */
|
||||||
|
static void case_mc_use_program(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i & 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
} else {
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- the case table both harnesses iterate --------------------------------
|
||||||
|
* a/b are the case's own knobs; opsPerFrame is what one bench frame is
|
||||||
|
* normalised by, so ns_per_op compares across renderers. The mc_* rates are
|
||||||
|
* the per-frame call counts measured from the captured traces.
|
||||||
|
*/
|
||||||
|
typedef void (*bench_case_fn)(int frame, long a, long b);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* name;
|
||||||
|
bench_case_fn fn;
|
||||||
|
long a, b, opsPerFrame;
|
||||||
|
} BenchCaseDesc;
|
||||||
|
|
||||||
|
static const BenchCaseDesc kBenchCases[] = {
|
||||||
|
{"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495},
|
||||||
|
{"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132},
|
||||||
|
{"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401},
|
||||||
|
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
|
||||||
|
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
|
||||||
|
{"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306},
|
||||||
|
{"mc_pass_switch", case_mc_pass_switch, 132, 0, 132},
|
||||||
|
{"mc_state_toggle", case_mc_state_toggle, 46, 0, 46},
|
||||||
|
{"mc_tex_param", case_mc_tex_param, 612, 0, 612},
|
||||||
|
{"mc_use_program", case_mc_use_program, 62, 0, 62},
|
||||||
|
{"draw_tiny", case_draw_tiny, 2048, 0, 2048},
|
||||||
|
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
|
||||||
|
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
|
||||||
|
{"tex_pingpong", case_tex_pingpong, 1024, 0, 1024},
|
||||||
|
{"program_pingpong", case_program_pingpong, 512, 0, 512},
|
||||||
|
{"chunk_upload", case_chunk_upload, 24, 0, 24},
|
||||||
|
{"atlas_sprite", case_atlas_sprite, 32, 0, 32},
|
||||||
|
{"lightmap", case_lightmap, 4, 0, 4},
|
||||||
|
{"scene_mix", case_scene_mix, 2048, 12, 2048},
|
||||||
|
};
|
||||||
|
static const int kBenchCaseCount = (int)(sizeof kBenchCases / sizeof kBenchCases[0]);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run the headless EGL DriverBench on one renderer:
|
||||||
|
# ./run_driver_bench.sh native [bench args...]
|
||||||
|
# ./run_driver_bench.sh espryt <libMobileGL.so> [bench args...]
|
||||||
|
# ./run_driver_bench.sh magma <libMobileGL.so> [bench args...]
|
||||||
|
# The bench dlopens exactly one EGL provider (DRIVERBENCH_EGL_LIB): the system
|
||||||
|
# libEGL.so.1 for native, or the given libMobileGL.so for a MobileGL backend -
|
||||||
|
# no LD_LIBRARY_PATH shadowing, so MobileGL's own loader still finds the real
|
||||||
|
# driver underneath.
|
||||||
|
#
|
||||||
|
# Pin the vendor libraries explicitly. A bare libEGL.so.1 on a glvnd system
|
||||||
|
# picks whatever vendor eglGetDisplay(EGL_DEFAULT_DISPLAY) resolves first,
|
||||||
|
# which is Mesa/llvmpipe here - a software rasteriser silently replacing the
|
||||||
|
# GPU under a benchmark. Override MGL_EGL_VENDOR / MGL_VK_ICD to test another
|
||||||
|
# driver.
|
||||||
|
set -eu
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
BENCH=${DRIVERBENCH_BIN:-$HERE/DriverBench}
|
||||||
|
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
|
||||||
|
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
|
||||||
|
MODE=$1; shift
|
||||||
|
|
||||||
|
export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
|
||||||
|
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
native)
|
||||||
|
export DRIVERBENCH_EGL_LIB=${DRIVERBENCH_EGL_LIB:-libEGL.so.1}
|
||||||
|
;;
|
||||||
|
espryt)
|
||||||
|
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectGLES
|
||||||
|
;;
|
||||||
|
magma)
|
||||||
|
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectVulkan
|
||||||
|
export VK_ICD_FILENAMES=$VK_ICD
|
||||||
|
;;
|
||||||
|
*) echo "unknown mode: $MODE (native|espryt|magma)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
exec "$BENCH" "$@"
|
||||||
@@ -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,
|
||||||
|
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) {
|
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,16 +1226,33 @@ 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);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||||
BufferOp::ClearNamedBufferSubData);
|
BufferOp::ClearNamedBufferSubData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,7 +53,9 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
namespace {
|
||||||
|
// The GL-visible number of indexed binding points for `target`.
|
||||||
|
SizeT GetBufferBindingPointLimit(BufferTarget target) {
|
||||||
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
||||||
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
||||||
const Int backendCount =
|
const Int backendCount =
|
||||||
@@ -65,6 +67,32 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
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) {
|
||||||
|
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||||
|
|
||||||
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->GetCurrentProgram();
|
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->GetCurrentProgram();
|
|
||||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -173,7 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||||
// is the tessellation pipeline's input and reaches the geometry stage already
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
// converted, so it is not constrained here.
|
// converted, so it is not constrained here.
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
@@ -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(
|
||||||
@@ -707,7 +847,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
const auto& program = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -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__) {
|
||||||
@@ -320,7 +320,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum prog
|
|||||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
|
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
|
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
|
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_STUB_END(GLuint, CreateShaderProgramv, type, count, strings)
|
DECLARE_GL_FUNCTION_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_END(GLuint, CreateShaderProgramv, type, count, strings)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
|
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
|
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
|
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
|
||||||
@@ -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});
|
||||||
@@ -1069,11 +1101,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case TextureTarget::Texture2DMultisampleArray:
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
|
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
|
||||||
break;
|
break;
|
||||||
|
case TextureTarget::Texture1DArray:
|
||||||
|
textureUploadTarget = TextureUploadTarget::Texture1DArray;
|
||||||
|
break;
|
||||||
|
case TextureTarget::TextureCubeMapArray:
|
||||||
|
textureUploadTarget = TextureUploadTarget::CubeMapArray;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
MG_State::pGLContext->RecordError(
|
||||||
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"FramebufferTextureLayer requires a 3D, array, 2D multisample "
|
||||||
|
"array, or cube map array texture."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The same backend question the DSA twin asks. GL 4.6 core 9.2.8 makes the two entry points
|
||||||
|
// equivalent, so they have to decline in the same places - leaving this one ungated is what
|
||||||
|
// let an unrepresentable attachment reach the renderer, and it also refused cube map arrays
|
||||||
|
// that GL requires it to accept.
|
||||||
|
{
|
||||||
|
const auto& layerLimits = MG_Backend::pActiveBackendObject
|
||||||
|
? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||||
|
: MG_Backend::DynamicBackendParameters{};
|
||||||
|
const TextureTarget layeredTarget = textureObject->GetTarget();
|
||||||
|
if ((layer != 0 || layeredTarget == TextureTarget::TextureCubeMapArray) &&
|
||||||
|
!layerLimits.SupportsPerLayerFramebufferAttachment(layeredTarget)) {
|
||||||
|
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||||
|
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
|
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1433,11 +1490,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// so a slice lands outside the image and the renderer asserts on the clear. Letting it
|
// so a slice lands outside the image and the renderer asserts on the clear. Letting it
|
||||||
// through there would only move the failure downstream, so it is declined instead - layer
|
// through there would only move the failure downstream, so it is declined instead - layer
|
||||||
// zero always works, being the plain first-slice attachment.
|
// zero always works, being the plain first-slice attachment.
|
||||||
const Bool backsLayeredAttachment = limits.SupportsPerLayerFramebufferAttachment;
|
// ...and it is a DIFFERENT question per target: a 2D/2D-multisample array layer is a Vulkan
|
||||||
// A cube map array additionally has no image shape at all in VkTextureManager, so on that
|
// array layer, a 3D layer is a z slice, and a cube map array needs a cube-compatible image
|
||||||
// backend it cannot be an attachment whatever the layer is.
|
// before it has any layer to name. Ask the backend about this texture's target rather than
|
||||||
const Bool isCubeMapArray = textureObject->GetTarget() == TextureTarget::TextureCubeMapArray;
|
// guessing from one blanket flag.
|
||||||
if ((layer != 0 && !backsLayeredAttachment) || (isCubeMapArray && !backsLayeredAttachment)) {
|
const TextureTarget layeredTextureTarget = textureObject->GetTarget();
|
||||||
|
const Bool backsThisTargetsLayers = limits.SupportsPerLayerFramebufferAttachment(layeredTextureTarget);
|
||||||
|
// Layer zero of a 3D or array texture is the plain first-slice attachment every backend can
|
||||||
|
// already express, so it stays legal even where per-layer selection is not backed. A cube map
|
||||||
|
// array has no such fallback: layer zero is still one face of one cube inside a
|
||||||
|
// cube-compatible image, so it needs the same support layer 5 does.
|
||||||
|
const Bool needsPerLayerSupport =
|
||||||
|
layer != 0 || layeredTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||||
|
if (needsPerLayerSupport && !backsThisTargetsLayers) {
|
||||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||||
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
|
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Vertex buffer binding index is out of range."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getInteger64i(target, index, data);
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
*data = vao ? static_cast<GLint64>(vao->GetBindingPoint(index).Offset) : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
@@ -174,6 +178,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
|
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
|
||||||
void ValidateProgram(GLuint program);
|
void ValidateProgram(GLuint program);
|
||||||
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
|
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
|
||||||
|
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings);
|
||||||
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
|
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
|
||||||
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
|
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
|
||||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
*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"));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,7 +354,64 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject) {
|
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||||
|
const char* caller) {
|
||||||
|
// A null object is somebody else's error to report - ValidateTextureObject runs
|
||||||
|
// first at every call site and has already recorded it.
|
||||||
|
if (!textureObject) return false;
|
||||||
|
|
||||||
|
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||||
|
if (mipmapTexture == nullptr) {
|
||||||
|
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
|
||||||
|
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
|
||||||
|
// set). Declining here is not the error code the spec asks for - that would be
|
||||||
|
// INVALID_ENUM from a target check this validator is not - but it does keep a
|
||||||
|
// texture with no image levels whatsoever from reaching a backend that would
|
||||||
|
// dereference a backend texture it never created.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture has no mipmap levels to address."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What this number is, exactly, because two other things are almost it and neither is
|
||||||
|
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
|
||||||
|
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
|
||||||
|
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
|
||||||
|
//
|
||||||
|
// The guarantee that matters is one-sided: this count is always >= the level count the
|
||||||
|
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
|
||||||
|
// with a non-positive extent, so it can only be shorter). That is the safe direction -
|
||||||
|
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
|
||||||
|
// exact match, so the backends keep their own range guard for the band in between: a
|
||||||
|
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
|
||||||
|
// declined by the backend, which is a silent no-op rather than a copy. That band is a
|
||||||
|
// backend storage limitation, not a validation one - rejecting it here with
|
||||||
|
// INVALID_VALUE would be refusing a copy the spec permits.
|
||||||
|
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||||
|
|
||||||
|
if (levelCount == 0) {
|
||||||
|
// No image has ever been defined on this texture, so the fault is the texture,
|
||||||
|
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
|
||||||
|
// copy names is an incomplete texture.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture has no image defined at any level."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Texture level does not exist in this texture."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||||
if (!textureObject) {
|
if (!textureObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -376,7 +434,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
TextureTarget target) {
|
TextureTarget target) {
|
||||||
if (!textureObject) return true; // should be created later
|
if (!textureObject) return true; // should be created later
|
||||||
TextureTarget prevTarget = textureObject->GetTarget();
|
TextureTarget prevTarget = textureObject->GetTarget();
|
||||||
@@ -390,7 +448,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
|
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
|
||||||
auto baseSize = textureObject->GetBaseSize();
|
auto baseSize = textureObject->GetBaseSize();
|
||||||
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
|
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
|
||||||
@@ -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,15 +30,53 @@ 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);
|
||||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
|
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
|
||||||
|
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
|
||||||
|
// about the object. Entry points that resolve a level straight into a backend image
|
||||||
|
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
|
||||||
|
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
|
||||||
|
// Note the error split is per-entry-point, so this is not universally reusable:
|
||||||
|
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
|
||||||
|
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
|
||||||
|
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||||
|
const char* caller);
|
||||||
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
// 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
|
||||||
// error is generated if zero is bound to target", ARB_texture_storage).
|
// error is generated if zero is bound to target", ARB_texture_storage).
|
||||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
const char* caller);
|
const char* caller);
|
||||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
TextureTarget target);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_VertexArray.h"
|
#include "GL_VertexArray.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Impl/GLImpl/Buffer/Validators.h>
|
#include <MG_Impl/GLImpl/Buffer/Validators.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
@@ -105,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.
|
||||||
@@ -163,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:
|
||||||
@@ -173,8 +211,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_CURRENT_VERTEX_ATTRIB:
|
case GL_CURRENT_VERTEX_ATTRIB:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
|
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
|
// Core since GL 4.1 (ARB_vertex_attrib_64bit). It was rejected while no attribute could
|
||||||
|
// ever be long; now that IsLong is real state the pname has to be accepted.
|
||||||
|
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(
|
||||||
@@ -269,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,
|
||||||
@@ -299,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) {
|
||||||
@@ -460,19 +509,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
relativeoffset, isBgra);
|
relativeoffset, isBgra);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The long (64-bit) attribute format. MobileGL has no 64-bit vertex attributes, so nothing is
|
// The long (64-bit) attribute format: the values reach the shader as doubles, unconverted
|
||||||
// recorded; what the entry point owes the application is the parameter validation, which is
|
// (GL 4.6 core 10.3.2). ValidateVertexAttribLFormat has already pinned type to GL_DOUBLE, so the
|
||||||
// observable through glGetError regardless of whether the format could be used in a draw.
|
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
||||||
static void VertexAttribLFormatSeparate_State(GLuint attribindex, GLint size, GLenum type,
|
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
||||||
|
//
|
||||||
|
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
||||||
|
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
||||||
|
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
||||||
|
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
||||||
|
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
|
GLuint attribindex, GLint size, GLenum type,
|
||||||
GLuint relativeoffset) {
|
GLuint relativeoffset) {
|
||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
|
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
|
||||||
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||||
|
|
||||||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
|
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||||
|
"backend has no double-precision vertex attribute support - see the "
|
||||||
|
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||||
|
attribindex);
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
||||||
"64-bit vertex attributes are not supported."));
|
"64-bit vertex attributes are not supported by this backend."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
||||||
|
/*normalized: */ false, /*isInteger: */ false, relativeoffset,
|
||||||
|
/*isBgra: */ false, /*isLong: */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
||||||
@@ -901,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));
|
||||||
@@ -915,9 +983,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? 1.0f : 0.0f;
|
params[0] = attr->IsInteger ? 1.0f : 0.0f;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? 1.0f : 0.0f;
|
||||||
|
return;
|
||||||
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,
|
||||||
@@ -961,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));
|
||||||
@@ -975,9 +1053,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? 1.0 : 0.0;
|
params[0] = attr->IsInteger ? 1.0 : 0.0;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? 1.0 : 0.0;
|
||||||
|
return;
|
||||||
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,
|
||||||
@@ -1016,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));
|
||||||
@@ -1031,9 +1122,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
|
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
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,
|
||||||
@@ -1068,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) {
|
||||||
@@ -1152,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));
|
||||||
@@ -1164,8 +1262,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
|
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
// 64-bit attributes are not supported, so no attribute is ever a long one.
|
*param = attr.IsLong ? GL_TRUE : GL_FALSE;
|
||||||
*param = GL_FALSE;
|
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
*param = static_cast<GLint>(attr.Divisor);
|
*param = static_cast<GLint>(attr.Divisor);
|
||||||
@@ -1173,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,
|
||||||
@@ -1222,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) {
|
||||||
@@ -1243,33 +1343,33 @@ 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(attribindex, size, type, relativeoffset);
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
|
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -1277,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.
|
||||||
|
|||||||
@@ -16,3 +16,256 @@ MOBILEGL_GLX_API void* glXGetProcAddress(const char* name) {
|
|||||||
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
|
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
|
||||||
return MG_Impl::GLXImpl::GetProcAddressARB(name);
|
return MG_Impl::GLXImpl::GetProcAddressARB(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
#include "../GLXImpl.h"
|
||||||
|
|
||||||
|
namespace GLXImpl = MobileGL::MG_Impl::GLXImpl;
|
||||||
|
|
||||||
|
// GLX handle/type spellings from GL/glx.h, expressed without including it:
|
||||||
|
// GLXContext/GLXFBConfig are opaque pointers, drawables are XIDs, Bool is int,
|
||||||
|
// and XVisualInfo* crosses as void*.
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryExtension(Display* dpy, int* errorBase, int* eventBase) {
|
||||||
|
return GLXImpl::QueryExtension(dpy, errorBase, eventBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryVersion(Display* dpy, int* major, int* minor) {
|
||||||
|
return GLXImpl::QueryVersion(dpy, major, minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXQueryExtensionsString(Display* dpy, int screen) {
|
||||||
|
return GLXImpl::QueryExtensionsString(dpy, screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXGetClientString(Display* dpy, int name) {
|
||||||
|
return GLXImpl::GetClientString(dpy, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXQueryServerString(Display* dpy, int screen, int name) {
|
||||||
|
return GLXImpl::QueryServerString(dpy, screen, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void** glXGetFBConfigs(Display* dpy, int screen, int* nelements) {
|
||||||
|
return GLXImpl::GetFBConfigs(dpy, screen, nelements);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void** glXChooseFBConfig(Display* dpy, int screen, const int* attribList,
|
||||||
|
int* nelements) {
|
||||||
|
return GLXImpl::ChooseFBConfig(dpy, screen, attribList, nelements);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetFBConfigAttrib(Display* dpy, void* config, int attribute, int* value) {
|
||||||
|
return GLXImpl::GetFBConfigAttrib(dpy, config, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXGetVisualFromFBConfig(Display* dpy, void* config) {
|
||||||
|
return GLXImpl::GetVisualFromFBConfig(dpy, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXChooseVisual(Display* dpy, int screen, int* attribList) {
|
||||||
|
return GLXImpl::ChooseVisual(dpy, screen, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetConfig(Display* dpy, void* visualInfo, int attribute, int* value) {
|
||||||
|
return GLXImpl::GetConfig(dpy, visualInfo, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateContext(Display* dpy, void* visualInfo, void* shareList, int direct) {
|
||||||
|
return GLXImpl::CreateContext(dpy, visualInfo, shareList, direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateNewContext(Display* dpy, void* config, int renderType,
|
||||||
|
void* shareList, int direct) {
|
||||||
|
return GLXImpl::CreateNewContext(dpy, config, renderType, shareList, direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateContextAttribsARB(Display* dpy, void* config, void* shareContext,
|
||||||
|
int direct, const int* attribList) {
|
||||||
|
return GLXImpl::CreateContextAttribsARB(dpy, config, shareContext, direct, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyContext(Display* dpy, void* context) {
|
||||||
|
GLXImpl::DestroyContext(dpy, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXMakeCurrent(Display* dpy, unsigned long drawable, void* context) {
|
||||||
|
return GLXImpl::MakeCurrent(dpy, drawable, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXMakeContextCurrent(Display* dpy, unsigned long draw, unsigned long read,
|
||||||
|
void* context) {
|
||||||
|
return GLXImpl::MakeContextCurrent(dpy, draw, read, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSwapBuffers(Display* dpy, unsigned long drawable) {
|
||||||
|
GLXImpl::SwapBuffers(dpy, drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreateWindow(Display* dpy, void* config, unsigned long window,
|
||||||
|
const int* attribList) {
|
||||||
|
return GLXImpl::CreateWindow(dpy, config, window, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyWindow(Display* dpy, unsigned long window) {
|
||||||
|
GLXImpl::DestroyWindow(dpy, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXGetCurrentContext() {
|
||||||
|
return GLXImpl::GetCurrentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXGetCurrentDrawable() {
|
||||||
|
return GLXImpl::GetCurrentDrawable();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXGetCurrentReadDrawable() {
|
||||||
|
return GLXImpl::GetCurrentReadDrawable();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API Display* glXGetCurrentDisplay() {
|
||||||
|
return GLXImpl::GetCurrentDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXIsDirect(Display* dpy, void* context) {
|
||||||
|
return GLXImpl::IsDirect(dpy, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXWaitGL() {
|
||||||
|
GLXImpl::WaitGL();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXWaitX() {
|
||||||
|
GLXImpl::WaitX();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryContext(Display* dpy, void* context, int attribute, int* value) {
|
||||||
|
return GLXImpl::QueryContext(dpy, context, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXQueryDrawable(Display* dpy, unsigned long drawable, int attribute,
|
||||||
|
unsigned int* value) {
|
||||||
|
GLXImpl::QueryDrawable(dpy, drawable, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSwapIntervalEXT(Display* dpy, unsigned long drawable, int interval) {
|
||||||
|
GLXImpl::SwapIntervalEXT(dpy, drawable, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXSwapIntervalMESA(unsigned int interval) {
|
||||||
|
return GLXImpl::SwapIntervalMESA(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetSwapIntervalMESA() {
|
||||||
|
return GLXImpl::GetSwapIntervalMESA();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||||
|
return GLXImpl::SwapIntervalSGI(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||||
|
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||||
|
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||||
|
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||||
|
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||||
|
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||||
|
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXGetSelectedEvent(Display*, unsigned long, unsigned long* eventMask) {
|
||||||
|
if (eventMask) {
|
||||||
|
*eventMask = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLXImpl {
|
||||||
|
namespace {
|
||||||
|
struct GLXEntryPoint {
|
||||||
|
const char* Name;
|
||||||
|
void* Proc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLXEntryPoint kGLXEntryPoints[] = {
|
||||||
|
{"glXChooseFBConfig", reinterpret_cast<void*>(glXChooseFBConfig)},
|
||||||
|
{"glXChooseVisual", reinterpret_cast<void*>(glXChooseVisual)},
|
||||||
|
{"glXCopyContext", reinterpret_cast<void*>(glXCopyContext)},
|
||||||
|
{"glXCreateContext", reinterpret_cast<void*>(glXCreateContext)},
|
||||||
|
{"glXCreateContextAttribsARB", reinterpret_cast<void*>(glXCreateContextAttribsARB)},
|
||||||
|
{"glXCreateGLXPixmap", reinterpret_cast<void*>(glXCreateGLXPixmap)},
|
||||||
|
{"glXCreateNewContext", reinterpret_cast<void*>(glXCreateNewContext)},
|
||||||
|
{"glXCreatePbuffer", reinterpret_cast<void*>(glXCreatePbuffer)},
|
||||||
|
{"glXCreatePixmap", reinterpret_cast<void*>(glXCreatePixmap)},
|
||||||
|
{"glXCreateWindow", reinterpret_cast<void*>(glXCreateWindow)},
|
||||||
|
{"glXDestroyContext", reinterpret_cast<void*>(glXDestroyContext)},
|
||||||
|
{"glXDestroyGLXPixmap", reinterpret_cast<void*>(glXDestroyGLXPixmap)},
|
||||||
|
{"glXDestroyPbuffer", reinterpret_cast<void*>(glXDestroyPbuffer)},
|
||||||
|
{"glXDestroyPixmap", reinterpret_cast<void*>(glXDestroyPixmap)},
|
||||||
|
{"glXDestroyWindow", reinterpret_cast<void*>(glXDestroyWindow)},
|
||||||
|
{"glXGetClientString", reinterpret_cast<void*>(glXGetClientString)},
|
||||||
|
{"glXGetConfig", reinterpret_cast<void*>(glXGetConfig)},
|
||||||
|
{"glXGetCurrentContext", reinterpret_cast<void*>(glXGetCurrentContext)},
|
||||||
|
{"glXGetCurrentDisplay", reinterpret_cast<void*>(glXGetCurrentDisplay)},
|
||||||
|
{"glXGetCurrentDrawable", reinterpret_cast<void*>(glXGetCurrentDrawable)},
|
||||||
|
{"glXGetCurrentReadDrawable", reinterpret_cast<void*>(glXGetCurrentReadDrawable)},
|
||||||
|
{"glXGetFBConfigAttrib", reinterpret_cast<void*>(glXGetFBConfigAttrib)},
|
||||||
|
{"glXGetFBConfigs", reinterpret_cast<void*>(glXGetFBConfigs)},
|
||||||
|
{"glXGetProcAddress", reinterpret_cast<void*>(glXGetProcAddress)},
|
||||||
|
{"glXGetProcAddressARB", reinterpret_cast<void*>(glXGetProcAddressARB)},
|
||||||
|
{"glXGetSelectedEvent", reinterpret_cast<void*>(glXGetSelectedEvent)},
|
||||||
|
{"glXGetSwapIntervalMESA", reinterpret_cast<void*>(glXGetSwapIntervalMESA)},
|
||||||
|
{"glXGetVisualFromFBConfig", reinterpret_cast<void*>(glXGetVisualFromFBConfig)},
|
||||||
|
{"glXIsDirect", reinterpret_cast<void*>(glXIsDirect)},
|
||||||
|
{"glXMakeContextCurrent", reinterpret_cast<void*>(glXMakeContextCurrent)},
|
||||||
|
{"glXMakeCurrent", reinterpret_cast<void*>(glXMakeCurrent)},
|
||||||
|
{"glXQueryContext", reinterpret_cast<void*>(glXQueryContext)},
|
||||||
|
{"glXQueryDrawable", reinterpret_cast<void*>(glXQueryDrawable)},
|
||||||
|
{"glXQueryExtension", reinterpret_cast<void*>(glXQueryExtension)},
|
||||||
|
{"glXQueryExtensionsString", reinterpret_cast<void*>(glXQueryExtensionsString)},
|
||||||
|
{"glXQueryServerString", reinterpret_cast<void*>(glXQueryServerString)},
|
||||||
|
{"glXQueryVersion", reinterpret_cast<void*>(glXQueryVersion)},
|
||||||
|
{"glXSelectEvent", reinterpret_cast<void*>(glXSelectEvent)},
|
||||||
|
{"glXSwapBuffers", reinterpret_cast<void*>(glXSwapBuffers)},
|
||||||
|
{"glXSwapIntervalEXT", reinterpret_cast<void*>(glXSwapIntervalEXT)},
|
||||||
|
{"glXSwapIntervalMESA", reinterpret_cast<void*>(glXSwapIntervalMESA)},
|
||||||
|
{"glXSwapIntervalSGI", reinterpret_cast<void*>(glXSwapIntervalSGI)},
|
||||||
|
{"glXUseXFont", reinterpret_cast<void*>(glXUseXFont)},
|
||||||
|
{"glXWaitGL", reinterpret_cast<void*>(glXWaitGL)},
|
||||||
|
{"glXWaitX", reinterpret_cast<void*>(glXWaitX)},
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void* GetGLXEntryPoint(const char* name) {
|
||||||
|
for (const auto& entry : kGLXEntryPoints) {
|
||||||
|
if (std::strcmp(entry.Name, name) == 0) {
|
||||||
|
return entry.Proc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||||
|
|
||||||
|
#endif // __linux__ && !__ANDROID__
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLXImpl/GLXImpl.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>
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLXImpl {
|
||||||
|
// GLX layered on MobileGL's own EGL, mirroring WGLImpl/CGLImpl. Handles are
|
||||||
|
// opaque to callers; XVisualInfo crosses the ABI as void* so this header
|
||||||
|
// needs no Xlib includes (Includes.h forward-declares Display/XID/Window).
|
||||||
|
using GLXFBConfigHandle = void*;
|
||||||
|
using GLXContextHandle = void*;
|
||||||
|
using GLXDrawableHandle = unsigned long; // XID
|
||||||
|
|
||||||
|
int QueryExtension(Display* dpy, int* errorBase, int* eventBase);
|
||||||
|
int QueryVersion(Display* dpy, int* major, int* minor);
|
||||||
|
const char* QueryExtensionsString(Display* dpy, int screen);
|
||||||
|
const char* GetClientString(Display* dpy, int name);
|
||||||
|
const char* QueryServerString(Display* dpy, int screen, int name);
|
||||||
|
|
||||||
|
GLXFBConfigHandle* GetFBConfigs(Display* dpy, int screen, int* nelements);
|
||||||
|
GLXFBConfigHandle* ChooseFBConfig(Display* dpy, int screen, const int* attribList, int* nelements);
|
||||||
|
int GetFBConfigAttrib(Display* dpy, GLXFBConfigHandle config, int attribute, int* value);
|
||||||
|
void* GetVisualFromFBConfig(Display* dpy, GLXFBConfigHandle config);
|
||||||
|
void* ChooseVisual(Display* dpy, int screen, int* attribList);
|
||||||
|
int GetConfig(Display* dpy, void* visualInfo, int attribute, int* value);
|
||||||
|
|
||||||
|
GLXContextHandle CreateContext(Display* dpy, void* visualInfo, GLXContextHandle share, int direct);
|
||||||
|
GLXContextHandle CreateNewContext(Display* dpy, GLXFBConfigHandle config, int renderType,
|
||||||
|
GLXContextHandle share, int direct);
|
||||||
|
GLXContextHandle CreateContextAttribsARB(Display* dpy, GLXFBConfigHandle config, GLXContextHandle share,
|
||||||
|
int direct, const int* attribList);
|
||||||
|
void DestroyContext(Display* dpy, GLXContextHandle context);
|
||||||
|
int MakeCurrent(Display* dpy, GLXDrawableHandle drawable, GLXContextHandle context);
|
||||||
|
int MakeContextCurrent(Display* dpy, GLXDrawableHandle draw, GLXDrawableHandle read,
|
||||||
|
GLXContextHandle context);
|
||||||
|
void SwapBuffers(Display* dpy, GLXDrawableHandle drawable);
|
||||||
|
|
||||||
|
GLXDrawableHandle CreateWindow(Display* dpy, GLXFBConfigHandle config, GLXDrawableHandle window,
|
||||||
|
const int* attribList);
|
||||||
|
void DestroyWindow(Display* dpy, GLXDrawableHandle window);
|
||||||
|
|
||||||
|
GLXContextHandle GetCurrentContext();
|
||||||
|
GLXDrawableHandle GetCurrentDrawable();
|
||||||
|
GLXDrawableHandle GetCurrentReadDrawable();
|
||||||
|
Display* GetCurrentDisplay();
|
||||||
|
int IsDirect(Display* dpy, GLXContextHandle context);
|
||||||
|
void WaitGL();
|
||||||
|
void WaitX();
|
||||||
|
int QueryContext(Display* dpy, GLXContextHandle context, int attribute, int* value);
|
||||||
|
void QueryDrawable(Display* dpy, GLXDrawableHandle drawable, int attribute, unsigned int* value);
|
||||||
|
|
||||||
|
void SwapIntervalEXT(Display* dpy, GLXDrawableHandle drawable, int interval);
|
||||||
|
int SwapIntervalMESA(unsigned int interval);
|
||||||
|
int GetSwapIntervalMESA();
|
||||||
|
int SwapIntervalSGI(int interval);
|
||||||
|
|
||||||
|
// Name -> exported glX entry point (table lives with the exports).
|
||||||
|
void* GetGLXEntryPoint(const char* name);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||||
|
|
||||||
|
#endif // __linux__ && !__ANDROID__
|
||||||
@@ -8,14 +8,30 @@
|
|||||||
|
|
||||||
#include "LookUp.h"
|
#include "LookUp.h"
|
||||||
|
|
||||||
namespace MG_Impl::GLXImpl {
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
// TODO: implement complete GLX functionality
|
#include "../GLXImpl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MG_Impl::GLXImpl {
|
||||||
void* GetProcAddress(const char* name) {
|
void* GetProcAddress(const char* name) {
|
||||||
|
if (!name) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
MGLOG_D("glXGetProcAddress(\"%s\")", name);
|
MGLOG_D("glXGetProcAddress(\"%s\")", name);
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
if (name[0] == 'g' && name[1] == 'l' && name[2] == 'X') {
|
||||||
|
// glX entry points resolve from the GLX layer's own table; GL/EGL
|
||||||
|
// names fall through to the shared resolver below.
|
||||||
|
void* proc = MobileGL::MG_Impl::GLXImpl::GetGLXEntryPoint(name);
|
||||||
|
if (!proc) {
|
||||||
|
MGLOG_D("glXGetProcAddress: unknown glX entry point %s", name);
|
||||||
|
}
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
#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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user