mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
Compare commits
237
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8025745fa3 | ||
|
|
4a533a215a | ||
|
|
0a5d7ceb6c | ||
|
|
ac81185968 | ||
|
|
9e861f3f7a | ||
|
|
da6f75dbd1 | ||
|
|
951da362f7 | ||
|
|
4c929b9b3f | ||
|
|
8d5072543a | ||
|
|
5de2b9e3e9 | ||
|
|
9192d156d1 | ||
|
|
27cdfbc0ca | ||
|
|
4567c3b468 | ||
|
|
d18c6a1bae | ||
|
|
dd745d7547 | ||
|
|
4407be89cd | ||
|
|
0c1a433af6 | ||
|
|
a2f3efe22c | ||
|
|
b9a15aed61 | ||
|
|
f748a06632 | ||
|
|
4532cae175 | ||
|
|
107b56d603 | ||
|
|
22b749dd37 | ||
|
|
f0c0211767 | ||
|
|
9ebbb76df1 | ||
|
|
95547ab9ce | ||
|
|
8eaf2d0069 | ||
|
|
54a8609c64 | ||
|
|
1fb0eb0737 | ||
|
|
282dd69230 | ||
|
|
e6ebe7078d | ||
|
|
30a91023f6 | ||
|
|
47dd8cdc05 | ||
|
|
8b36a15fb3 | ||
|
|
07fa84fb8d | ||
|
|
a03817b4ee | ||
|
|
641bc0cdd9 | ||
|
|
c069890ac7 | ||
|
|
48dd1c5956 | ||
|
|
a389477f78 | ||
|
|
94a8f1e3f3 | ||
|
|
45d506545e | ||
|
|
d9d63c9496 | ||
|
|
92140405c1 | ||
|
|
b9ecfef0b6 | ||
|
|
fba26ea169 | ||
|
|
93a3b55907 | ||
|
|
d1487bedf0 | ||
|
|
1bb736c57e | ||
|
|
eb76686c1e | ||
|
|
1a04fb8c0c | ||
|
|
306790ee7c | ||
|
|
170ccda3e7 | ||
|
|
e86a9bbec5 | ||
|
|
c5569e71b3 | ||
|
|
7e8c32a063 | ||
|
|
77bd03d962 | ||
|
|
37111ae992 | ||
|
|
6b0c2a15ab | ||
|
|
e9ffd99313 | ||
|
|
7c01ddea0c | ||
|
|
2d4d6e9cfb | ||
|
|
76b8957b99 | ||
|
|
ec685b9fa7 | ||
|
|
a12068df52 | ||
|
|
0b344792cc | ||
|
|
9fa32bdad0 | ||
|
|
a4980f2b56 | ||
|
|
8ca20e28ca | ||
|
|
c353a2055f | ||
|
|
421c20984e | ||
|
|
fc4cd980f2 | ||
|
|
992d16267c | ||
|
|
0ea9e6de5f | ||
|
|
241ed377b4 | ||
|
|
bf312a4b67 | ||
|
|
56b31a9587 | ||
|
|
8a0a8a0274 | ||
|
|
d076c29146 | ||
|
|
930a607bdf | ||
|
|
34685b4bb0 | ||
|
|
c540fb88ee | ||
|
|
6ae3245a0d | ||
|
|
7e048fc2bf | ||
|
|
83cdfd6bdd | ||
|
|
1c76f886cf | ||
|
|
a2e109beff | ||
|
|
63f0756644 | ||
|
|
450215d12c | ||
|
|
3a9e520170 | ||
|
|
d2996ba1cf | ||
|
|
c8632dfefe | ||
|
|
b8a8a660e1 | ||
|
|
7ab83861ca | ||
|
|
eaeba556a3 | ||
|
|
72fa1221a5 | ||
|
|
c4254c4bbd | ||
|
|
199164c2e0 | ||
|
|
e87063e90c | ||
|
|
122da27249 | ||
|
|
bc2d698b3e | ||
|
|
3049c4b82b | ||
|
|
2b3850b76b | ||
|
|
2a0ae743a0 | ||
|
|
6839219c10 | ||
|
|
52ddb440ca | ||
|
|
79feeffd25 | ||
|
|
202037b5a3 | ||
|
|
bce9c48c8e | ||
|
|
b6a7807a3a | ||
|
|
48ba622387 | ||
|
|
05260d1262 | ||
|
|
6eb5ff51c5 | ||
|
|
e526f8e8ac | ||
|
|
e724e88eec | ||
|
|
3b175fb88a | ||
|
|
b5a4e7075a | ||
|
|
520c2b6750 | ||
|
|
57cc652b1d | ||
|
|
65ea54da9e | ||
|
|
c81dd04f08 | ||
|
|
c158bfa584 | ||
|
|
f9f455144c | ||
|
|
64e4840de2 | ||
|
|
bf7b5755cc | ||
|
|
293f64b3c2 | ||
|
|
f0cc07c937 | ||
|
|
4658536652 | ||
|
|
04b4627c65 | ||
|
|
68e13705c8 | ||
|
|
e5388c0e7e | ||
|
|
8bc4808b1a | ||
|
|
5d8a5387e2 | ||
|
|
aa2184e47a | ||
|
|
9152a4a4bc | ||
|
|
1963b427db | ||
|
|
f3def150e7 | ||
|
|
fc0688c223 | ||
|
|
626c7f26fd | ||
|
|
1d947d934b | ||
|
|
4203837648 | ||
|
|
f5cba4c2f1 | ||
|
|
c2a1db3fcb | ||
|
|
594916850f | ||
|
|
c718d6bad8 | ||
|
|
abfda60ff1 | ||
|
|
92cced9bcc | ||
|
|
1929a7c546 | ||
|
|
df7f5a369d | ||
|
|
0de9861da4 | ||
|
|
6b223e4d23 | ||
|
|
48568cdb89 | ||
|
|
8d83dedc0f | ||
|
|
a25d8ee0e7 | ||
|
|
c30bd0fabb | ||
|
|
c9fbf79d6a | ||
|
|
f39e8738da | ||
|
|
c6d22e6ece | ||
|
|
981f10e4da | ||
|
|
076cd0d19d | ||
|
|
5cd82f2002 | ||
|
|
d4922cb0fb | ||
|
|
870d882fef | ||
|
|
e2f873c95c | ||
|
|
efd7b47388 | ||
|
|
a08669df72 | ||
|
|
b8db509581 | ||
|
|
5d6b544021 | ||
|
|
346cd417ca | ||
|
|
f61675e9ce | ||
|
|
8026838563 | ||
|
|
5bd8fa8c4e | ||
|
|
37ef2cb600 | ||
|
|
5184901a5b | ||
|
|
4172959e49 | ||
|
|
eb5b4bca3e | ||
|
|
56db115a1a | ||
|
|
b14edd515a | ||
|
|
57030b5fb9 | ||
|
|
72532de780 | ||
|
|
25323bfb8e | ||
|
|
9ed5dbf483 | ||
|
|
3ca57068a8 | ||
|
|
075471cdbd | ||
|
|
62a0ad5639 | ||
|
|
8bf8f6f906 | ||
|
|
249c1ca574 | ||
|
|
6a843b3088 | ||
|
|
2e94314b78 | ||
|
|
c59c15f66a | ||
|
|
4d1613ba55 | ||
|
|
254cf1dc21 | ||
|
|
607e84deed | ||
|
|
df0e9fca71 | ||
|
|
b831dae8d5 | ||
|
|
0005a50517 | ||
|
|
9f302373d6 | ||
|
|
f896c7396f | ||
|
|
6ca48e40fe | ||
|
|
1cefb9780b | ||
|
|
a1a8a18575 | ||
|
|
bae222227a | ||
|
|
37a050b106 | ||
|
|
12a67f596c | ||
|
|
164bfd810b | ||
|
|
274c234aff | ||
|
|
b8dc4a6004 | ||
|
|
20e1b417cc | ||
|
|
3e8c8b756f | ||
|
|
4dbdbd3bcd | ||
|
|
763d4c3207 | ||
|
|
5eeba29579 | ||
|
|
9351d66dbc | ||
|
|
dae8a40af1 | ||
|
|
e9bd520d9b | ||
|
|
8b78379f6f | ||
|
|
1dc217b32c | ||
|
|
535e9f8b15 | ||
|
|
35626da5c4 | ||
|
|
b9844ed7c1 | ||
|
|
8496e7c7eb | ||
|
|
f0ed5c1b8e | ||
|
|
c247ad5807 | ||
|
|
b604188849 | ||
|
|
7514587b5a | ||
|
|
cf8f928db8 | ||
|
|
176d130f09 | ||
|
|
0b94e02de5 | ||
|
|
fe7a5ee1b2 | ||
|
|
5331150cb9 | ||
|
|
dc2f3a477b | ||
|
|
21753b0e4c | ||
|
|
f509b19b1d | ||
|
|
c7ac5de28e | ||
|
|
2e7073a890 | ||
|
|
305701326c | ||
|
|
8c89b1618a |
@@ -9,17 +9,45 @@ fi
|
||||
case_name="$1"
|
||||
fixture_dir="${2:-tools/trace_replay/fixtures}"
|
||||
python_bin="${PYTHON:-python3}"
|
||||
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
|
||||
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
|
||||
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
|
||||
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
|
||||
default_mirror_bases=(
|
||||
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
|
||||
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
|
||||
)
|
||||
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
|
||||
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
|
||||
else
|
||||
mirror_bases=("${default_mirror_bases[@]}")
|
||||
fi
|
||||
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
|
||||
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
|
||||
fi
|
||||
# Optional bearer token for mirrors that require authentication (private Gitea).
|
||||
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
|
||||
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
|
||||
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
|
||||
|
||||
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
|
||||
python_bin=python
|
||||
fi
|
||||
|
||||
if ! [[ "${download_attempts}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS must be a positive integer: ${download_attempts}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! [[ "${retry_delay}" =~ ^[0-9]+$ ]]; then
|
||||
echo "MOBILEGL_TRACE_FIXTURE_RETRY_DELAY must be a non-negative integer: ${retry_delay}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
|
||||
--format fixture-files \
|
||||
--case "${case_name}" \
|
||||
--fixture-root "${fixture_dir}")"
|
||||
mapfile -t files <<< "${fixture_list}"
|
||||
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
|
||||
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
|
||||
|
||||
include="$(IFS=,; echo "${files[*]}")"
|
||||
if [ "${case_name}" = "OpenRA" ]; then
|
||||
@@ -34,34 +62,178 @@ if [ "${case_name}" = "OpenRA" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_file_from_mirror() {
|
||||
local file="$1"
|
||||
local url="$2"
|
||||
local metadata
|
||||
local expected_oid
|
||||
local expected_size
|
||||
local tmp_file="${file}.tmp"
|
||||
local attempt
|
||||
local partial_size
|
||||
local curl_status
|
||||
local curl_auth
|
||||
|
||||
metadata="$(get_lfs_metadata "${file}")" || return 1
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
|
||||
if [ -f "${tmp_file}" ]; then
|
||||
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
|
||||
if [ "${partial_size}" -gt "${expected_size}" ]; then
|
||||
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
elif [ "${partial_size}" = "${expected_size}" ]; then
|
||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||
mv "${tmp_file}" "${file}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${tmp_file}"
|
||||
fi
|
||||
fi
|
||||
|
||||
for ((attempt = 1; attempt <= download_attempts; attempt++)); do
|
||||
partial_size=0
|
||||
if [ -f "${tmp_file}" ]; then
|
||||
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
|
||||
fi
|
||||
|
||||
if [ "${partial_size}" -gt 0 ]; then
|
||||
echo "Resuming mirror download for ${file} at byte ${partial_size} (attempt ${attempt}/${download_attempts})"
|
||||
else
|
||||
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
|
||||
fi
|
||||
|
||||
curl_auth=()
|
||||
if [ -n "${mirror_token}" ]; then
|
||||
curl_auth=(--header "Authorization: token ${mirror_token}")
|
||||
fi
|
||||
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
|
||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||
mv "${tmp_file}" "${file}"
|
||||
return 0
|
||||
fi
|
||||
echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
else
|
||||
curl_status=$?
|
||||
partial_size=0
|
||||
if [ -f "${tmp_file}" ]; then
|
||||
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
|
||||
fi
|
||||
|
||||
if [ "${partial_size}" = "${expected_size}" ]; then
|
||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||
mv "${tmp_file}" "${file}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
elif [ "${partial_size}" -gt "${expected_size}" ]; then
|
||||
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
elif [ "${curl_status}" -eq 33 ]; then
|
||||
echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
fi
|
||||
|
||||
echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2
|
||||
fi
|
||||
|
||||
if [ "${attempt}" -lt "${download_attempts}" ]; then
|
||||
sleep "${retry_delay}"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -f "${tmp_file}"
|
||||
return 1
|
||||
}
|
||||
|
||||
fetch_from_mirror() {
|
||||
mkdir -p "${fixture_dir}"
|
||||
for file in "${files[@]}"; do
|
||||
local name
|
||||
local url
|
||||
local base
|
||||
local fetched=0
|
||||
name="$(basename "${file}")"
|
||||
url="${mirror_base%/}/${name}"
|
||||
echo "Fetching trace fixture from mirror: ${url}"
|
||||
if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
|
||||
rm -f "${file}.tmp"
|
||||
for base in "${mirror_bases[@]}"; do
|
||||
url="${base%/}/${name}"
|
||||
echo "Fetching trace fixture from mirror: ${url}"
|
||||
if fetch_file_from_mirror "${file}" "${url}"; then
|
||||
fetched=1
|
||||
break
|
||||
fi
|
||||
echo "Mirror did not serve ${name}; trying the next mirror" >&2
|
||||
done
|
||||
if [ "${fetched}" -ne 1 ]; then
|
||||
return 1
|
||||
fi
|
||||
mv "${file}.tmp" "${file}"
|
||||
done
|
||||
}
|
||||
|
||||
if fetch_from_mirror; then
|
||||
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
||||
else
|
||||
echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
|
||||
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
|
||||
git lfs install --local
|
||||
git lfs pull --include="${include}" --exclude=""
|
||||
fi
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
test -s "${file}"
|
||||
if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
|
||||
echo "failed to hydrate LFS fixture: ${file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
metadata="$(get_lfs_metadata "${file}")"
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
|
||||
done
|
||||
|
||||
+60
-32
@@ -227,6 +227,8 @@ jobs:
|
||||
env:
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -245,11 +247,11 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ env.ANDROID_AVD_HOME }}
|
||||
/usr/local/lib/android/sdk/emulator
|
||||
/usr/local/lib/android/sdk/platform-tools
|
||||
/usr/local/lib/android/sdk/platforms/android-35
|
||||
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
${{ env.ANDROID_SDK_ROOT }}/emulator
|
||||
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
||||
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
||||
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
|
||||
- name: Create AVD
|
||||
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
||||
@@ -274,6 +276,8 @@ jobs:
|
||||
env:
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
@@ -288,7 +292,7 @@ jobs:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 16
|
||||
swap-size-gb: 8
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -324,11 +328,11 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ env.ANDROID_AVD_HOME }}
|
||||
/usr/local/lib/android/sdk/emulator
|
||||
/usr/local/lib/android/sdk/platform-tools
|
||||
/usr/local/lib/android/sdk/platforms/android-35
|
||||
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
${{ env.ANDROID_SDK_ROOT }}/emulator
|
||||
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
||||
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
||||
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
|
||||
- name: Download retrace APK
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -378,27 +382,51 @@ jobs:
|
||||
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--coherent-as-flush)
|
||||
fi
|
||||
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
||||
--apk-file "${apk_file}" \
|
||||
--package top.mobilegl.plugin.trace \
|
||||
--backend "${{ matrix.backend.name }}" \
|
||||
--result-root android-retrace-result \
|
||||
--fixture-root android-retrace-fixture \
|
||||
--case "${{ matrix.case.name }}" \
|
||||
--trace-archive "${{ matrix.case.trace_archive }}" \
|
||||
--trace-file "${{ matrix.case.trace_file }}" \
|
||||
--golden "${{ matrix.case.golden }}" \
|
||||
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
|
||||
--target-call "${{ matrix.case.target_call }}" \
|
||||
--width "${{ matrix.case.width }}" \
|
||||
--height "${{ matrix.case.height }}" \
|
||||
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
|
||||
--crop-x "${{ matrix.case.crop_x }}" \
|
||||
--crop-y "${{ matrix.case.crop_y }}" \
|
||||
--crop-width "${{ matrix.case.crop_width }}" \
|
||||
--crop-height "${{ matrix.case.crop_height }}" \
|
||||
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
|
||||
"${extra_retrace_args[@]}"
|
||||
|
||||
run_retrace() {
|
||||
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
||||
--apk-file "${apk_file}" \
|
||||
--package top.mobilegl.plugin.trace \
|
||||
--backend "${{ matrix.backend.name }}" \
|
||||
--result-root android-retrace-result \
|
||||
--fixture-root android-retrace-fixture \
|
||||
--case "${{ matrix.case.name }}" \
|
||||
--trace-archive "${{ matrix.case.trace_archive }}" \
|
||||
--trace-file "${{ matrix.case.trace_file }}" \
|
||||
--golden "${{ matrix.case.golden }}" \
|
||||
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
|
||||
--target-call "${{ matrix.case.target_call }}" \
|
||||
--width "${{ matrix.case.width }}" \
|
||||
--height "${{ matrix.case.height }}" \
|
||||
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
|
||||
--crop-x "${{ matrix.case.crop_x }}" \
|
||||
--crop-y "${{ matrix.case.crop_y }}" \
|
||||
--crop-width "${{ matrix.case.crop_width }}" \
|
||||
--crop-height "${{ matrix.case.crop_height }}" \
|
||||
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
|
||||
"${extra_retrace_args[@]}"
|
||||
}
|
||||
|
||||
retrace_status=0
|
||||
run_retrace || retrace_status=$?
|
||||
if [ "${retrace_status}" -eq 75 ]; then
|
||||
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
--pid-file "${EMULATOR_PID_FILE}"
|
||||
adb kill-server || true
|
||||
sleep 2
|
||||
sh android-plugin/run-avd-ci.sh start \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--gpu "${{ matrix.backend.gpu }}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
--pid-file "${EMULATOR_PID_FILE}" \
|
||||
--boot-timeout 300
|
||||
run_retrace
|
||||
elif [ "${retrace_status}" -ne 0 ]; then
|
||||
exit "${retrace_status}"
|
||||
fi
|
||||
|
||||
- name: Collect retrace summary inputs
|
||||
if: always()
|
||||
|
||||
@@ -107,6 +107,7 @@ jobs:
|
||||
--exclude='build.ninja' \
|
||||
--exclude='cmake_install.cmake' \
|
||||
-czf ci-artifacts/mobilegl-linux-runtime.tgz \
|
||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||
"${SHARED_LIBS[@]}"
|
||||
@@ -156,12 +157,12 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Test
|
||||
working-directory: build-linux/MobileGL/MG_Test
|
||||
working-directory: build-linux
|
||||
run: |
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V
|
||||
ctest -V -L unit --no-tests=error
|
||||
else
|
||||
ctest --output-on-failure
|
||||
ctest --output-on-failure -L unit --no-tests=error
|
||||
fi
|
||||
|
||||
benchmark:
|
||||
@@ -202,8 +203,8 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Benchmark
|
||||
working-directory: build-linux/MobileGL/MG_Benchmark
|
||||
run: ctest -V -C Release
|
||||
working-directory: build-linux
|
||||
run: ctest -V -C Release -L benchmark --no-tests=error
|
||||
|
||||
build-retrace:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -454,6 +455,15 @@ jobs:
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||
fi
|
||||
# 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
|
||||
# bypasses only the vendor gate, so this exercises the real strip on
|
||||
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
|
||||
# unstripped path covered for the same trace.
|
||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
|
||||
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
|
||||
fi
|
||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||
|
||||
- name: Upload actual image
|
||||
|
||||
@@ -25,3 +25,5 @@ MobileGL/MG*/cmake-build*
|
||||
/android-plugin/app/src/trace/jniLibs
|
||||
/android-plugin/local.properties
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 26fe5ceb45...900b29d449
+55
-2
@@ -188,8 +188,12 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -299,6 +303,13 @@ if (ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
|
||||
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
set(MOBILEGL_LINK_LIBRARIES
|
||||
glslang::glslang
|
||||
spirv-cross-c
|
||||
@@ -327,10 +338,18 @@ set(MOBILEGL_INCLUDE_DIR
|
||||
${SPIRV-Headers_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
# The wgl* entry points are exported via .def (see the comment in wgl.def);
|
||||
# only the shared library links it.
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
|
||||
)
|
||||
endif()
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
|
||||
C_VISIBILITY_PRESET default
|
||||
@@ -373,6 +392,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
|
||||
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
|
||||
# pixel-format forwarding will resolve.
|
||||
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
|
||||
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
|
||||
COMMENT "Creating opengl32.dll drop-in copy"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||
${SOURCE_FILES}
|
||||
@@ -424,8 +455,21 @@ if (ANDROID)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
|
||||
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
|
||||
# symbols interposes incompatible copies embedded by host libraries such
|
||||
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
|
||||
# visible; GetProcAddress can still return pointers to hidden internals.
|
||||
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
|
||||
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
@@ -433,6 +477,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
"-framework Cocoa"
|
||||
"-framework CoreVideo"
|
||||
"-framework QuartzCore"
|
||||
"-framework Foundation"
|
||||
"-framework OpenGL"
|
||||
@@ -475,6 +520,15 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
|
||||
endif ()
|
||||
|
||||
if (NOT ANDROID)
|
||||
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
|
||||
# at the build-tree root. This lets `ctest` be invoked from the top-level
|
||||
# build directory (IDE "run all tests", CI) and discover every test in the
|
||||
# subdirectories below, instead of having to descend into each
|
||||
# MG_Test/MG_Benchmark subdirectory. Tests are tagged with CTest labels
|
||||
# (unit / benchmark / integration), so e.g. `ctest -L unit` selects just
|
||||
# the unit suite.
|
||||
enable_testing()
|
||||
|
||||
if (MOBILEGL_BUILD_TEST)
|
||||
add_subdirectory(MobileGL/MG_Test)
|
||||
endif()
|
||||
@@ -484,7 +538,6 @@ if (NOT ANDROID)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_TRACE_REPLAY)
|
||||
enable_testing()
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+31
-1
@@ -14,12 +14,21 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
extern BackendType ActiveBackendType;
|
||||
|
||||
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
|
||||
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
|
||||
// device gate - each quirk keeps its structural safety checks.
|
||||
enum class QuirkOverride : Uint8 {
|
||||
Auto = 0,
|
||||
ForceOn,
|
||||
ForceOff,
|
||||
};
|
||||
|
||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||
@@ -61,6 +70,27 @@ namespace MobileGL::MG_Config {
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -86,6 +86,17 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
|
||||
}
|
||||
|
||||
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
|
||||
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
|
||||
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return MG_Config::QuirkOverride::Auto;
|
||||
}
|
||||
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
|
||||
: MG_Config::QuirkOverride::ForceOff;
|
||||
}
|
||||
|
||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
@@ -122,6 +133,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#define MOBILEGL_EGL_API MOBILEGL_API
|
||||
#define MOBILEGL_CGL_API MOBILEGL_API
|
||||
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
|
||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||
|
||||
// ====================== MobileGL configurations ======================= //
|
||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
|
||||
@@ -14,7 +14,13 @@ namespace MobileGL {
|
||||
} // namespace MG_Config
|
||||
|
||||
namespace MG_Backend {
|
||||
UniquePtr<BackendObject> pActiveBackendObject;
|
||||
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
|
||||
// never destroyed by the runtime, so process exit runs no backend
|
||||
// destructors (static destruction order across TUs is undefined).
|
||||
// Deterministic teardown happens inside the EGL lifecycle instead:
|
||||
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
|
||||
// these singletons while the process is still healthy.
|
||||
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
|
||||
GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
} // namespace MG_Backend
|
||||
} // namespace MobileGL
|
||||
|
||||
+47
-33
@@ -9,14 +9,25 @@
|
||||
#include "Init.h"
|
||||
#include "Config.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace {
|
||||
Bool g_isInitialized = false;
|
||||
std::atomic<Bool> g_isInitialized = false;
|
||||
thread_local Bool tl_initializing = false;
|
||||
|
||||
std::mutex& InitMutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
void DestroyImpl(Bool logLifecycle) {
|
||||
if (!g_isInitialized) {
|
||||
@@ -27,6 +38,12 @@ namespace MobileGL {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
glslang::FinalizeProcess();
|
||||
// GL syncs die with their contexts, and every context is gone by the
|
||||
// time full teardown runs: drain the live-sync registry while the
|
||||
// backend function table can still release the backend handles (and
|
||||
// before a re-initialized library could pair them with the wrong
|
||||
// backend's DeleteSync).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
@@ -64,40 +81,37 @@ namespace MobileGL {
|
||||
MGLOG_I("MobileGL initialized");
|
||||
}
|
||||
|
||||
void EnsureInitialized() {
|
||||
if (g_isInitialized.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
// Re-entrant call while this thread is already inside Initialize()
|
||||
// (e.g. an init step routing back through a public entry point).
|
||||
if (tl_initializing) {
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(InitMutex());
|
||||
if (g_isInitialized.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
tl_initializing = true;
|
||||
Initialize();
|
||||
tl_initializing = false;
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
DestroyImpl(true);
|
||||
}
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
__attribute__((constructor)) static void AutoInit() {
|
||||
Initialize();
|
||||
}
|
||||
|
||||
__attribute__((destructor)) static void AutoDestroy() {
|
||||
if (MG_Config::Features.TraceSkipAutodestroy) {
|
||||
return;
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
|
||||
return;
|
||||
#else
|
||||
DestroyImpl(false);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
|
||||
switch (ul_reason_for_call) {
|
||||
case DLL_PROCESS_ATTACH:
|
||||
Initialize();
|
||||
break;
|
||||
|
||||
case DLL_PROCESS_DETACH:
|
||||
Destroy();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
// MobileGL's lifecycle is owned entirely by the host-API layers
|
||||
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
||||
// via EnsureInitialized(), and full teardown happens deterministically
|
||||
// when the last EGL display is terminated with nothing current (EGLImpl
|
||||
// calls Destroy()). There is intentionally no backend-initializing static
|
||||
// constructor, no static destructor, and no DllMain: the global singletons
|
||||
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||
// without eglTerminate simply leaks them to the OS instead of running
|
||||
// backend destructors during static teardown. macOS has a lightweight
|
||||
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
|
||||
// initialization still enters here from the first hooked CGL context.
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
|
||||
namespace MobileGL {
|
||||
void Initialize();
|
||||
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
|
||||
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
|
||||
// full backend initialization never depends on ELF/DLL static constructors,
|
||||
// and so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate). The macOS dyld bootstrap installs only lightweight
|
||||
// NSOpenGL method hooks.
|
||||
void EnsureInitialized();
|
||||
void Destroy();
|
||||
|
||||
namespace MG_Util::Debug {
|
||||
|
||||
@@ -220,6 +220,15 @@ namespace MobileGL {
|
||||
// and leave the query readable later.
|
||||
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
||||
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
|
||||
// the frontend then rejects the target). Results/deletion flow through
|
||||
// GetQueryResult64 / DeleteBackendQuery like timer queries.
|
||||
BackendQueryHandle (*BeginOcclusionQuery)();
|
||||
void (*EndOcclusionQuery)(BackendQueryHandle query);
|
||||
// Transform feedback primitive queries backed by real GPU query pools
|
||||
// (optional; null = frontend falls back to CPU accounting).
|
||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||
};
|
||||
struct GlobalBackendFunctionsTable {
|
||||
@@ -230,8 +239,26 @@ namespace MobileGL {
|
||||
void (*SetSwapInterval)(Int interval);
|
||||
};
|
||||
|
||||
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
|
||||
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
|
||||
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
|
||||
enum class GpuVendorKind : Uint8 {
|
||||
Unknown = 0,
|
||||
Qualcomm,
|
||||
Arm,
|
||||
Nvidia,
|
||||
Amd,
|
||||
Intel,
|
||||
ImgTec,
|
||||
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
|
||||
Software,
|
||||
};
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
Float SmoothLineWidthRangeMin = 1.0f;
|
||||
@@ -269,6 +296,9 @@ namespace MobileGL {
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
Int MaxCombinedImageUniforms = 8;
|
||||
Int MaxVertexImageUniforms = 0;
|
||||
Int MaxGeometryImageUniforms = 0;
|
||||
Int MaxFragmentImageUniforms = 8;
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
@@ -279,19 +309,28 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
// GL 4.x fragment-interpolation offset limits. These defaults are the
|
||||
// core minimums and are replaced by live GLES/Vulkan device limits.
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
// For four fractional bits the greatest required legal offset is
|
||||
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
Uint32 SubgroupSupportedFeatures = 0;
|
||||
Bool SubgroupQuadOperationsInAllStages = false;
|
||||
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
|
||||
};
|
||||
|
||||
enum class WindowBackend {
|
||||
Android,
|
||||
X11,
|
||||
MetalLayer,
|
||||
// TODO: Wayland, Windows, etc.
|
||||
Win32, // Handle is an HWND
|
||||
// TODO: Wayland, etc.
|
||||
WindowBackendCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
#include "DirectVulkan/BackendObject_DirectVulkan.h"
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
extern UniquePtr<BackendObject> pActiveBackendObject;
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -603,11 +604,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
||||
.TargetGLVersion = {3, 3, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
|
||||
.Extensions = BuildAdvertisedExtensions(false),
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -627,8 +628,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedTimerQueryExtension() {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -672,11 +674,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
|
||||
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
|
||||
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
|
||||
// the extension list is first built).
|
||||
UpdateAdvertisedTimerQueryExtension();
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -700,9 +702,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if ((handle.Backend != WindowBackend::Android &&
|
||||
handle.Backend != WindowBackend::X11 &&
|
||||
handle.Backend != WindowBackend::MetalLayer) ||
|
||||
handle.Backend != WindowBackend::MetalLayer &&
|
||||
handle.Backend != WindowBackend::Win32) ||
|
||||
!handle.Handle) {
|
||||
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
|
||||
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -818,14 +821,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
||||
E_GL_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_direct_state_access,
|
||||
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_gpu_shader5, E_GL_ARB_multi_bind,
|
||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||
@@ -836,6 +839,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
// state is accepted regardless, but forwarding it would be a no-op without the extension,
|
||||
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
|
||||
// get plain trilinear.
|
||||
if (anisotropicFilteringSupported) {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -924,11 +935,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
|
||||
// and share the handle-based result/delete entries, which must exist even
|
||||
// when the timer-query group above is disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -938,8 +954,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return m_dynamicParameters;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
|
||||
const MG_External::GLESCapabilities& capabilities) {
|
||||
m_GLESCapabilities = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
|
||||
@@ -993,9 +1016,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
||||
const Int maxSupportedTextureUnits =
|
||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms;
|
||||
m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms;
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
|
||||
const auto clampStageImageUniforms = [this](Int stageLimit) {
|
||||
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
|
||||
m_dynamicParameters.MaxCombinedImageUniforms});
|
||||
};
|
||||
m_dynamicParameters.MaxVertexImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
|
||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
|
||||
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
@@ -1005,8 +1040,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
|
||||
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_GLESCapabilities.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
|
||||
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset =
|
||||
m_GLESCapabilities.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_GLESCapabilities.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines =
|
||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
||||
|
||||
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
|
||||
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
|
||||
return haystack.find(needle) != String::npos;
|
||||
});
|
||||
};
|
||||
const String vendorAndRenderer =
|
||||
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
|
||||
if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) {
|
||||
// Check software rasterizers first: ANGLE-on-llvmpipe reports both.
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
||||
} else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
|
||||
} else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
|
||||
} else if (containsAny(vendorAndRenderer, {"NVIDIA"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
|
||||
} else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
|
||||
} else if (containsAny(vendorAndRenderer, {"Intel"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
|
||||
} else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
||||
} else {
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
|
||||
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
|
||||
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
|
||||
|
||||
private:
|
||||
void UpdateDynamicBackendParameters();
|
||||
@@ -66,9 +67,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries are (or are not) usable. The
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,10 @@
|
||||
operation Utils::CheckGLESError();
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Re-establishes the frontend texture-unit bindings on the native ES context.
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -126,6 +130,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
|
||||
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
|
||||
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||
// null or stale-generation handles IS final: the frontend may cache it
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||
@@ -177,6 +178,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
||||
void BindBufferId(GLenum target, Uint id);
|
||||
void InvalidateArrayBufferBindingCache();
|
||||
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
|
||||
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
|
||||
// upload site routes its binding through these so the shadow always matches
|
||||
// the driver; the resting state between operations is 0, which keeps any
|
||||
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
|
||||
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
|
||||
// and a recycled name matching the shadow would false-skip the rebind) and
|
||||
// invalidated on MakeCurrent (context may reset).
|
||||
void BindPixelPackBufferId(Uint id);
|
||||
void BindPixelUnpackBufferId(Uint id);
|
||||
void InvalidatePixelBufferBindingCaches();
|
||||
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
|
||||
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||
// could false-skip when the name is recycled.
|
||||
void NoteBufferIdDeleted(Uint id);
|
||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||
// 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/
|
||||
@@ -254,10 +270,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace TextureImpl {
|
||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||
if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
|
||||
target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray)
|
||||
return false;
|
||||
return true;
|
||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
||||
// coordinate padding for 1D/1D-array shaders.
|
||||
return target != TextureTarget::TextureRectangle;
|
||||
}
|
||||
|
||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
default:
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
|
||||
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
|
||||
}
|
||||
|
||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||
switch (uploadTarget) {
|
||||
case TextureUploadTarget::Texture1D:
|
||||
return GL_TEXTURE_2D;
|
||||
case TextureUploadTarget::Texture1DArray:
|
||||
return GL_TEXTURE_2D_ARRAY;
|
||||
default:
|
||||
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
|
||||
// moves the layer count into depth.
|
||||
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
return {texelSize.x(), 1, texelSize.y()};
|
||||
}
|
||||
return texelSize;
|
||||
}
|
||||
|
||||
inline Bool IsMultisampleTextureTarget(TextureTarget target) {
|
||||
@@ -293,6 +347,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
class BackendTextureObject {
|
||||
public:
|
||||
BackendTextureObject();
|
||||
// Deletes the GL texture (frontend glDeleteTextures used to leak every
|
||||
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
|
||||
// shadows so a recycled name or heap address cannot false-skip a rebind.
|
||||
~BackendTextureObject();
|
||||
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
@@ -304,6 +364,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void RecreateBackendTexture();
|
||||
|
||||
Uint m_backendTextureId = 0;
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
@@ -328,6 +391,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
extern Uint g_activeTextureUnit;
|
||||
// Bumped when the backend ES context is destroyed; texture ids stamped with
|
||||
// an older generation belong to a dead context and must not be deleted.
|
||||
extern Uint g_textureContextGeneration;
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {
|
||||
@@ -336,6 +402,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendFramebufferObject();
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||
FramebufferTarget asTarget);
|
||||
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
|
||||
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
|
||||
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
|
||||
void InvalidateSyncedState();
|
||||
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
|
||||
void Bind(FramebufferTarget target) const;
|
||||
@@ -369,8 +439,105 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
g_backendFramebufferObjects;
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
||||
// 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
|
||||
// must re-sync it even when the binding-slot version has not moved.
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects;
|
||||
|
||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||
// query forces a driver pipeline sync) and so redundant rebinds no-op.
|
||||
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
|
||||
// the driver once. Invalidated on MakeCurrent (context may reset).
|
||||
// GL_FRAMEBUFFER binds both targets.
|
||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||
void InvalidateFramebufferBindingCache();
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
|
||||
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
|
||||
// texture) is detached exactly when a later use of another aspect would
|
||||
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
|
||||
// incomplete and silently degraded later readbacks).
|
||||
namespace ScratchFBOImpl {
|
||||
struct ScratchFramebuffer {
|
||||
Uint id = 0;
|
||||
// false => attachment state unknown; scrub every point on next use.
|
||||
// A fresh FBO starts with nothing attached, so creation sets it true.
|
||||
Bool attachmentsKnown = false;
|
||||
Uint colorTex = 0;
|
||||
GLenum colorTarget = 0;
|
||||
GLint colorLevel = 0;
|
||||
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
|
||||
Uint depthTex = 0;
|
||||
GLenum depthTarget = 0;
|
||||
GLint depthLevel = 0;
|
||||
Bool depthHasStencil = false;
|
||||
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
|
||||
GLenum readBuffer = 0;
|
||||
GLenum drawBuffer = 0;
|
||||
};
|
||||
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
|
||||
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
|
||||
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
|
||||
// Returns the GL id, generating it if needed (requires a current ES context).
|
||||
Uint EnsureId(ScratchFramebuffer& fb);
|
||||
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
|
||||
// target the READ/DRAW binding respectively). Each Ensure* performs the
|
||||
// minimal detach/attach set and keeps the shadow in sync; a failed attach
|
||||
// records the point as detached so the completeness check fails instead of
|
||||
// silently reading a stale attachment.
|
||||
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
|
||||
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
|
||||
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
||||
Bool withStencil);
|
||||
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
|
||||
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
|
||||
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
|
||||
// binding while respecifying texture storage). Attachment is set once at
|
||||
// creation and never changes.
|
||||
Uint EnsureCompleteTinyFramebufferId();
|
||||
// A backend texture id is being deleted or respecified: a scratch FBO still
|
||||
// referencing it would hold a dangling attachment (ES only auto-detaches
|
||||
// from the *bound* framebuffer), and a recycled name could false-skip a
|
||||
// re-attach; force a full scrub on next use.
|
||||
void NoteTextureIdDeleted(Uint textureId);
|
||||
// The ES context (and the scratch FBO ids with it) is going away.
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace ScratchFBOImpl
|
||||
|
||||
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
|
||||
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
|
||||
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
|
||||
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
|
||||
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
|
||||
// pins the driver to the shadow by writing all fields once. Invalidated on
|
||||
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
|
||||
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
|
||||
// frontend context state instead.
|
||||
namespace PixelStoreImpl {
|
||||
struct PackState {
|
||||
GLint Alignment = 4;
|
||||
GLint RowLength = 0;
|
||||
GLint SkipRows = 0;
|
||||
GLint SkipPixels = 0;
|
||||
Bool operator==(const PackState& o) const {
|
||||
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
|
||||
SkipPixels == o.SkipPixels;
|
||||
}
|
||||
};
|
||||
void ApplyPackState(const PackState& desired);
|
||||
PackState CurrentPackState();
|
||||
void InvalidatePackStateCache();
|
||||
} // namespace PixelStoreImpl
|
||||
|
||||
// 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
|
||||
// assigned through glUniform1i.
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
@@ -448,4 +452,407 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
} // namespace Utils
|
||||
|
||||
// ---- Client-format readback conversion helpers -------------------------------------------------
|
||||
// ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack
|
||||
// it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so
|
||||
// unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror
|
||||
// the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers).
|
||||
namespace ReadbackImpl {
|
||||
using MG_Util::DecodeHalfBitsToFloat;
|
||||
using MG_Util::EncodeFloatToHalfBits;
|
||||
|
||||
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) {
|
||||
switch (format) {
|
||||
case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true;
|
||||
// Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue):
|
||||
// the destination holds one component sourced from the named channel of the wide RGBA read.
|
||||
// GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the
|
||||
// legacy alpha-texture upload hack.
|
||||
case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true;
|
||||
case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true;
|
||||
case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true;
|
||||
case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true;
|
||||
case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true;
|
||||
case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true;
|
||||
case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true;
|
||||
case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true;
|
||||
case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true;
|
||||
case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) {
|
||||
switch (type) {
|
||||
// Non-REV types pack the first format component starting at the most significant bit,
|
||||
// *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5:
|
||||
// R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15).
|
||||
case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true;
|
||||
case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true;
|
||||
case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true;
|
||||
// Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit
|
||||
// exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field.
|
||||
case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true;
|
||||
case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SizeT GetReadbackComponentSize(GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (GetPackedReadbackLayout(type, packedLayout)) {
|
||||
return packedLayout.byteSize;
|
||||
}
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
case GL_BYTE:
|
||||
return 1;
|
||||
case GL_UNSIGNED_SHORT:
|
||||
case GL_SHORT:
|
||||
case GL_HALF_FLOAT:
|
||||
return 2;
|
||||
case GL_UNSIGNED_INT:
|
||||
case GL_INT:
|
||||
case GL_FLOAT:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (GetPackedReadbackLayout(type, packedLayout)) {
|
||||
if (packedLayout.fieldCount != mapping.channelCount) {
|
||||
return 0; // 3-field packed types pair with 3-component formats only, 4 with 4
|
||||
}
|
||||
if (mapping.isInteger && packedLayout.isFloatPacked) {
|
||||
return 0; // packed-float RGB types never pair with integer formats
|
||||
}
|
||||
return packedLayout.byteSize;
|
||||
}
|
||||
if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) {
|
||||
return 0;
|
||||
}
|
||||
const SizeT componentSize = GetReadbackComponentSize(type);
|
||||
return componentSize == 0 ? 0 : static_cast<SizeT>(mapping.channelCount) * componentSize;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
|
||||
switch (byteSize) {
|
||||
case 1: {
|
||||
const auto out = static_cast<Uint8>(word);
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
const auto out = static_cast<Uint16>(word);
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Memcpy(dst, &word, sizeof(word));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
|
||||
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
|
||||
Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); }
|
||||
Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); }
|
||||
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); }
|
||||
|
||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||
const ReadbackChannelMapping& mapping, GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPacked = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
|
||||
|
||||
for (SizeT col = 0; col < width; ++col) {
|
||||
const Uint8* srcPixel = src + col * srcPixelBytes;
|
||||
Uint8* dstPixel = dst + col * dstPixelBytes;
|
||||
if (mapping.isInteger) {
|
||||
Int64 srcValues[4];
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = wideType == GL_INT
|
||||
? static_cast<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
|
||||
: static_cast<Int64>(reinterpret_cast<const Uint32*>(srcPixel)[c]);
|
||||
}
|
||||
if (isPacked) {
|
||||
// Integer sources clamp each component to the unsigned range of its field
|
||||
// (GL 3.3 section 4.3.1 final conversion).
|
||||
Uint32 word = 0;
|
||||
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
|
||||
const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1;
|
||||
const auto v = static_cast<Uint32>(
|
||||
std::clamp<Int64>(srcValues[mapping.sourceChannel[ch]], 0, fieldMax));
|
||||
word |= v << packedLayout.shift[ch];
|
||||
}
|
||||
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
|
||||
} else {
|
||||
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
|
||||
const Int64 v = srcValues[mapping.sourceChannel[ch]];
|
||||
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
|
||||
break;
|
||||
case GL_BYTE: {
|
||||
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_SHORT: {
|
||||
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_SHORT: {
|
||||
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_INT: {
|
||||
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_INT: {
|
||||
const auto out =
|
||||
static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Float srcValues[4];
|
||||
switch (wideType) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = static_cast<Float>(srcPixel[c]) / 255.0f;
|
||||
}
|
||||
break;
|
||||
case GL_BYTE:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = std::max(
|
||||
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
|
||||
}
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] =
|
||||
static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
|
||||
}
|
||||
break;
|
||||
case GL_SHORT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = std::max(
|
||||
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
|
||||
}
|
||||
break;
|
||||
case GL_HALF_FLOAT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
|
||||
}
|
||||
break;
|
||||
default: // GL_FLOAT
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = reinterpret_cast<const Float*>(srcPixel)[c];
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isPacked) {
|
||||
Uint32 word = 0;
|
||||
if (packedLayout.isFloatPacked) {
|
||||
const Float fields[3] = {srcValues[mapping.sourceChannel[0]],
|
||||
srcValues[mapping.sourceChannel[1]],
|
||||
srcValues[mapping.sourceChannel[2]]};
|
||||
word = type == GL_UNSIGNED_INT_5_9_9_9_REV
|
||||
? EncodeSharedExponentRGB9E5(fields)
|
||||
: (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) |
|
||||
(EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) |
|
||||
(EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]);
|
||||
} else {
|
||||
// Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field.
|
||||
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
|
||||
const auto fieldMax = static_cast<Float>((1u << packedLayout.width[ch]) - 1u);
|
||||
const auto v = static_cast<Uint32>(std::llround(
|
||||
std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax));
|
||||
word |= v << packedLayout.shift[ch];
|
||||
}
|
||||
}
|
||||
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
|
||||
} else {
|
||||
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
|
||||
const Float v = srcValues[mapping.sourceChannel[ch]];
|
||||
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
*dstComponent =
|
||||
static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
|
||||
break;
|
||||
case GL_BYTE: {
|
||||
const auto out =
|
||||
static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_SHORT: {
|
||||
const auto out =
|
||||
static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_SHORT: {
|
||||
const auto out =
|
||||
static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_INT: {
|
||||
const auto out = static_cast<Uint32>(
|
||||
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_INT: {
|
||||
const auto out = static_cast<Int32>(
|
||||
std::llround(static_cast<Double>(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_FLOAT:
|
||||
Memcpy(dstComponent, &v, sizeof(v));
|
||||
break;
|
||||
case GL_HALF_FLOAT: {
|
||||
const Uint16 out = EncodeFloatToHalfBits(v);
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
|
||||
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
|
||||
return (rowBytes + align - 1) / align * align;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||
// Per the GL addressing rules, slice k row j lands at
|
||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
|
||||
if (packParams.SwapBytes) {
|
||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||
if (groupSize > 1) {
|
||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace ReadbackImpl
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -45,6 +45,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
|
||||
// Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
|
||||
// RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
|
||||
// exercise the exact packing the GL CTS packed_pixels oracle compares against.
|
||||
namespace ReadbackImpl {
|
||||
struct ReadbackChannelMapping {
|
||||
Int sourceChannel[4]; // RGBA source channel feeding each destination component
|
||||
Int channelCount; // destination component count
|
||||
Bool isInteger;
|
||||
};
|
||||
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);
|
||||
|
||||
// Byte size of one destination component of `type`; packed types report the packed word size.
|
||||
// 0 = type not supported by the conversion path.
|
||||
SizeT GetReadbackComponentSize(GLenum type);
|
||||
|
||||
// Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
|
||||
// component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
|
||||
// the packed word: non-REV types pack the first component from the MSB, *_REV types from the
|
||||
// LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
|
||||
struct PackedReadbackLayout {
|
||||
Int fieldCount; // format components stored in the packed word
|
||||
Int width[4]; // bit width of each component's field
|
||||
Int shift[4]; // LSB bit position of each component's field
|
||||
SizeT byteSize; // packed word size in bytes (1, 2 or 4)
|
||||
Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
|
||||
};
|
||||
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);
|
||||
|
||||
// Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
|
||||
Uint32 EncodeFloatToUnsignedF11(Float value);
|
||||
Uint32 EncodeFloatToUnsignedF10(Float value);
|
||||
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);
|
||||
|
||||
// Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
|
||||
// not convertible (unknown type, packed field count != format component count, floating-point
|
||||
// or packed-float type with an integer format).
|
||||
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);
|
||||
|
||||
// Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
|
||||
// client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
|
||||
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||
const ReadbackChannelMapping& mapping, GLenum type);
|
||||
|
||||
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
|
||||
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
|
||||
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
|
||||
// DirectGLES and DirectVulkan readback conversion paths.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode);
|
||||
String ForceSupporterOutput(const String& glslCode);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -140,6 +141,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
return TextureInternalFormat::RGBA8;
|
||||
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
|
||||
// encoding; a wider normalized fallback keeps at least the required precision.
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
return TextureInternalFormat::RGBA8;
|
||||
case TextureInternalFormat::RGB10:
|
||||
return TextureInternalFormat::RGB10A2;
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGBA12:
|
||||
return TextureInternalFormat::RGBA16;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return TextureInternalFormat::SRGB8Alpha8;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
@@ -397,8 +412,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
|
||||
handle.Backend != WindowBackend::X11 &&
|
||||
handle.Backend != WindowBackend::MetalLayer)) {
|
||||
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
|
||||
handle.Backend != WindowBackend::MetalLayer &&
|
||||
handle.Backend != WindowBackend::Win32)) {
|
||||
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -454,6 +470,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
@@ -463,6 +482,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
||||
@@ -488,14 +510,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
.IsCompatibilityProfile = false
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
@@ -503,10 +526,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||
E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location};
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -516,6 +540,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Only advertised when the samplerAnisotropy device feature was granted: without it the
|
||||
// sampler state is accepted but never applied, and an app trusting the string (LWJGL builds
|
||||
// GLCapabilities from it) would think it enabled anisotropic filtering.
|
||||
if (anisotropicFilteringSupported) {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -605,6 +636,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries share the handle-based result/delete entries, which must
|
||||
// exist even when timer queries are disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -630,7 +670,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -680,6 +721,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
// rather than a maximum the sampler manager will never apply.
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
||||
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
||||
: 1.0f;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
||||
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
||||
@@ -732,9 +778,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
|
||||
m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||
const Int maxPerStageImageUniforms =
|
||||
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
||||
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
|
||||
// optional device features. VulkanRenderer enables each feature whenever the physical
|
||||
// device reports it, so these are the exact limits the logical device can compile and run.
|
||||
m_dynamicParameters.MaxVertexImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
|
||||
? maxPerStageImageUniforms
|
||||
: 0;
|
||||
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
||||
const Int maxSupportedDrawBuffers =
|
||||
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||
@@ -746,6 +807,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
|
||||
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_vulkanCaps.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
||||
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_vulkanCaps.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
@@ -765,5 +843,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps.MaxShaderStorageBlockSize,
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize);
|
||||
}
|
||||
switch (m_vulkanCaps.VendorId) {
|
||||
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
|
||||
break;
|
||||
case 0x13B5u: // ARM
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
|
||||
break;
|
||||
case 0x10DEu: // NVIDIA
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
|
||||
break;
|
||||
case 0x1002u: // AMD
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
|
||||
break;
|
||||
case 0x8086u: // Intel
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
|
||||
break;
|
||||
case 0x1010u: // Imagination
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
||||
break;
|
||||
case 0x10005u: // Mesa software (lavapipe)
|
||||
case 0x1AE0u: // Google (SwiftShader)
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
||||
break;
|
||||
default:
|
||||
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -73,7 +73,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
|
||||
|
||||
namespace {
|
||||
// Generation of the live VulkanRenderer instance, mirroring
|
||||
@@ -60,6 +61,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
struct ProgramResourceCache {
|
||||
// Lifetime id of the program the cached reflection belongs to. GL names are
|
||||
// recycled (IndexGenerator hands freed indices straight back), and a
|
||||
// recreated program's backendStateVersion restarts at the same small values,
|
||||
// so the version alone can collide; the never-reused lifetime id makes the
|
||||
// slot's ownership unambiguous.
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 backendStateVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
@@ -81,6 +88,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 baseInstance = 0;
|
||||
};
|
||||
|
||||
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
|
||||
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
|
||||
// checked against the program's lifetime id before it is served (see
|
||||
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
|
||||
// ClearProgramResourceCaches.
|
||||
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
||||
|
||||
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
@@ -141,13 +153,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||
if (cache.backendStateVersion == backendStateVersion &&
|
||||
// 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
|
||||
// count from zero) would otherwise be served the dead program's reflection.
|
||||
if (cache.programLifetimeId == programLifetimeId &&
|
||||
cache.backendStateVersion == backendStateVersion &&
|
||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = {};
|
||||
cache.programLifetimeId = programLifetimeId;
|
||||
cache.backendStateVersion = backendStateVersion;
|
||||
|
||||
Vector<SpvReflectShaderModule> modules;
|
||||
@@ -365,6 +383,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ClearProgramResourceCaches() {
|
||||
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
|
||||
// calls are serialized in this codebase (contexts migrate threads but never
|
||||
// run concurrently), so no other thread can be inside the unsynchronized map.
|
||||
// Live programs in another context self-heal: their entry rebuilds from the
|
||||
// retained generated SPIR-V on the next resource query.
|
||||
g_programResourceCaches.clear();
|
||||
}
|
||||
|
||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||
auto& cache = GetProgramResourceCache(program);
|
||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||
@@ -1223,10 +1250,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
pVulkanRenderer->Clear(mask);
|
||||
}
|
||||
|
||||
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
|
||||
// whose synthesized index list revisits the first vertex at the end.
|
||||
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = GL_LINE_STRIP;
|
||||
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
|
||||
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
|
||||
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
|
||||
payload.indexBufferView.forceClientMemory = true;
|
||||
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
|
||||
payload.params.instanceCount = 1;
|
||||
payload.params.vertexOffset = basevertex;
|
||||
pVulkanRenderer->DrawElements(payload);
|
||||
}
|
||||
|
||||
// Resolve a DrawElements index list (bound element-array buffer or client
|
||||
// memory) into uint32 values with the loop-closing first index appended.
|
||||
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
|
||||
Vector<Uint32>& outIndices) {
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0 || count < 2) {
|
||||
return false;
|
||||
}
|
||||
const Uint8* indexBytes = nullptr;
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (indexBufferShared != nullptr) {
|
||||
const SizeT offset = reinterpret_cast<SizeT>(indices);
|
||||
const SizeT bufferSize = indexBufferShared->GetSize();
|
||||
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
|
||||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
|
||||
return false;
|
||||
}
|
||||
indexBufferShared->SyncPersistentMappedRange();
|
||||
indexBytes = indexBufferShared->MappedData() + offset;
|
||||
} else {
|
||||
indexBytes = static_cast<const Uint8*>(indices);
|
||||
if (indexBytes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
outIndices.resize(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
switch (indexSize) {
|
||||
case 1: outIndices[i] = indexBytes[i]; break;
|
||||
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
|
||||
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
|
||||
}
|
||||
}
|
||||
outIndices[count] = outIndices[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
closedIndices[i] = static_cast<Uint32>(first + i);
|
||||
}
|
||||
closedIndices[count] = static_cast<Uint32>(first);
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.params.firstVertex = first;
|
||||
@@ -1239,6 +1332,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1307,6 +1408,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1456,8 +1564,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// records are shared (SharedPtr) with the owning pool's pending list,
|
||||
// so deleting the query while results are still in flight is safe.
|
||||
struct VulkanTimerQuery {
|
||||
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
|
||||
Kind kind = Kind::Timer;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1547,6 +1659,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ever be produced, so resolve with a final 0.
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
|
||||
Uint64 samples = 0;
|
||||
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
|
||||
return false;
|
||||
}
|
||||
query->occlusionSlots.clear(); // slots are recycled by the resolve
|
||||
*outNanoseconds = samples;
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
return true;
|
||||
}
|
||||
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
||||
// complete until Present submits the commands, so the wait refuses to
|
||||
// block on the current unsubmitted serial. Returning false keeps the
|
||||
@@ -1579,6 +1711,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
delete static_cast<VulkanTimerQuery*>(handle);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = VulkanTimerQuery::Kind::Occlusion;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndOcclusionQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
|
||||
}
|
||||
|
||||
Int64 GetGpuTimestampNs() {
|
||||
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
||||
// exist as vkCmdWriteTimestamp results read back later, and
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "Renderer/VulkanRenderer.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
|
||||
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
|
||||
|
||||
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
||||
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||
@@ -23,6 +23,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
// safe because GL calls are serialized in this codebase, and any still-live
|
||||
// program rebuilds its entry from the retained generated SPIR-V on demand.
|
||||
void ClearProgramResourceCaches();
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -117,6 +123,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only while a live renderer exists whose device can actually time.
|
||||
Bool IsTimerQuerySupported();
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
|
||||
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = device;
|
||||
m_commandPool = commandPool;
|
||||
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = frameCount;
|
||||
allocInfo.commandBufferCount = frameCount * 2;
|
||||
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
m_frames[i].commandBuffer = commandBuffers[i];
|
||||
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
|
||||
}
|
||||
|
||||
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
||||
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
commandBuffers[i] = m_frames[i].commandBuffer;
|
||||
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
|
||||
}
|
||||
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||
}
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
||||
GetCurrent().isCommandRecording = false;
|
||||
GetCurrent().hasCommandBufferRecorded = false;
|
||||
GetCurrent().isPreCommandRecording = false;
|
||||
GetCurrent().hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
||||
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
|
||||
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
|
||||
void FrameContext::EndPreCommandRecordingIfOpen() {
|
||||
auto& frame = GetCurrent();
|
||||
if (!frame.isPreCommandRecording) {
|
||||
return;
|
||||
}
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
void FrameContext::AbandonPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
|
||||
}
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
|
||||
DestroySwapchainSemaphores(device);
|
||||
if (swapchainImageCount == 0) {
|
||||
@@ -150,12 +189,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
|
||||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& commandBuffer = BeginCommandRecording();
|
||||
// The barrier belongs in the frame's own recording. Bailing out because
|
||||
// something was already recorded (the previous behaviour) dropped the
|
||||
// transition entirely for every frame that never ran a default-framebuffer
|
||||
// render pass - the only other thing that carries the image to
|
||||
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
|
||||
// handed to the WSI still in the layout it was acquired in.
|
||||
// A closed-but-unsubmitted buffer can only come from a submit that already
|
||||
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
|
||||
// appending to it is illegal while reopening would reset the frame's own
|
||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||
// rather than trade a lost device for a barrier into a closed buffer.
|
||||
if (frame.hasCommandBufferRecorded) {
|
||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reopening a recording here would vkResetCommandBuffer this frame's own
|
||||
// commands away, so append to the open one and let the caller close it.
|
||||
const Bool openedRecording = !frame.isCommandRecording;
|
||||
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
|
||||
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
@@ -174,7 +231,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
EndCommandRecording();
|
||||
if (openedRecording) {
|
||||
EndCommandRecording();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -182,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 swapchainImageIndex) const {
|
||||
const auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"GetSubmitInfo called while the pre-pass stream is still recording");
|
||||
AssertValidSwapchainImageIndex(swapchainImageIndex);
|
||||
SubmitInfoPacket packet{};
|
||||
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
||||
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
|
||||
packet.commandBuffer = frame.commandBuffer;
|
||||
|
||||
Uint32 commandBufferCount = 0;
|
||||
// The pre-pass stream executes strictly before the frame's commands.
|
||||
if (frame.hasPreCommandBufferRecorded) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
|
||||
}
|
||||
if (shouldSubmitCommandBuffer) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
|
||||
}
|
||||
|
||||
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
|
||||
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
||||
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
||||
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
|
||||
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
|
||||
packet.submitInfo.commandBufferCount = commandBufferCount;
|
||||
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
|
||||
packet.submitInfo.signalSemaphoreCount = 1;
|
||||
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
||||
return packet;
|
||||
@@ -227,12 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||
&outImageIndex);
|
||||
if (result != VK_SUCCESS) {
|
||||
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
|
||||
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
|
||||
// the consumed-flag reset (leaving a stale "already consumed", so the next
|
||||
// submit never waited on the pending signal) and the fence reset (leaving
|
||||
// the slot's fence signaled for the next submit to reuse). Only a genuine
|
||||
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
|
||||
// and nothing is signaled - skips the bookkeeping.
|
||||
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
|
||||
return result;
|
||||
}
|
||||
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
return vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
||||
}
|
||||
|
||||
Uint32 FrameContext::GetCurrentFrameIndex() const {
|
||||
@@ -247,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_recordingObserver = observer;
|
||||
}
|
||||
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer() {
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
||||
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
||||
auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the command buffer is still recording");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
@@ -260,11 +340,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
frame.preCommandBuffer = preReplacement;
|
||||
}
|
||||
// lastSubmitIndex was just written by the renderer for the submission
|
||||
// that carried this command buffer.
|
||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
||||
frame.commandBuffer = replacement;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
@@ -274,12 +366,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
|
||||
frame.retiredCommandBuffers.data());
|
||||
for (const auto& retired : frame.retiredCommandBuffers) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
|
||||
}
|
||||
}
|
||||
frame.retiredCommandBuffers.clear();
|
||||
}
|
||||
|
||||
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
|
||||
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
for (auto& frame : m_frames) {
|
||||
// Retired buffers are appended in submit order, so the completed
|
||||
// ones form a prefix.
|
||||
SizeT completedCount = 0;
|
||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
++completedCount;
|
||||
}
|
||||
if (completedCount > 0) {
|
||||
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
|
||||
frame.retiredCommandBuffers.begin() + completedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::FreeAllRetiredCommandBuffers() {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
// command buffer; submitInfo.pCommandBuffers points here.
|
||||
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
};
|
||||
|
||||
@@ -40,17 +42,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||
};
|
||||
|
||||
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
|
||||
// with the submit-tracker index it was submitted under so it can be
|
||||
// freed as soon as that submission is observed complete - without
|
||||
// waiting for the slot's fence to be waited again (present-less flush
|
||||
// loops never wait it).
|
||||
struct RetiredCommandBuffer {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Uint64 submitIndex = 0;
|
||||
};
|
||||
|
||||
struct FrameData {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// Pre-pass work stream: out-of-pass commands (deferred clear
|
||||
// materialization, sampled-layout transitions) for resources the
|
||||
// frame's recording has not touched yet. Submitted immediately
|
||||
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
|
||||
// into it never has to split the frame's active render pass.
|
||||
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
|
||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
||||
Bool isCommandRecording = false;
|
||||
Bool hasCommandBufferRecorded = false;
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands) whose
|
||||
// execution is only known complete once this slot's fence has been
|
||||
// waited again; freed at that point.
|
||||
Vector<VkCommandBuffer> retiredCommandBuffers;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
// complete (fence wait or completion poll).
|
||||
Vector<RetiredCommandBuffer> retiredCommandBuffers;
|
||||
// Submit-tracker index of this slot's most recent queue submission
|
||||
// (written by the renderer at submit time).
|
||||
Uint64 lastSubmitIndex = 0;
|
||||
@@ -67,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
||||
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
||||
void EndCommandRecording();
|
||||
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
|
||||
VkCommandBuffer BeginPreCommandRecording();
|
||||
// Closes the pre stream if open, marking it for submission ahead of the
|
||||
// frame command buffer. Safe to call when it never opened.
|
||||
void EndPreCommandRecordingIfOpen();
|
||||
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
|
||||
// frame recordings, swapchain recreation).
|
||||
void AbandonPreCommandRecording();
|
||||
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
|
||||
void DestroySwapchainSemaphores(VkDevice device);
|
||||
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
|
||||
@@ -79,8 +107,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Parks the current (already ended and submitted) command buffer on the
|
||||
// slot's retired list and installs a freshly allocated one, so recording
|
||||
// can restart while the submitted buffer is still executing. Retired
|
||||
// buffers are freed after the slot's fence is next waited.
|
||||
VkResult RetireCurrentCommandBuffer();
|
||||
// buffers are freed after the slot's fence is next waited, or as soon
|
||||
// as their submission is observed complete.
|
||||
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
|
||||
|
||||
// Frees every retired command buffer whose tagged submission index is
|
||||
// known complete. Driven by the renderer's submit tracker on completion
|
||||
// events (fence waits and non-blocking polls), so present-less flush
|
||||
// loops reclaim their buffers without any extra wait.
|
||||
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
|
||||
// Frees every slot's retired command buffers. Only valid when the
|
||||
// caller has proven every queue submission complete.
|
||||
void FreeAllRetiredCommandBuffers();
|
||||
|
||||
Uint32 GetCurrentFrameIndex() const;
|
||||
Uint32 GetFrameCount() const;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
switch (topology) {
|
||||
@@ -108,6 +110,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"vkCreatePipelineCache");
|
||||
}
|
||||
|
||||
// Must be called once, before any pipeline is created: the flag is not part of the
|
||||
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
|
||||
// old value.
|
||||
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
|
||||
s_suppressBlendedDepthWrite = enabled;
|
||||
}
|
||||
|
||||
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
|
||||
Uint32 vendorId) {
|
||||
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||
switch (quirkOverride) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
return true;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
return false;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
default:
|
||||
return vendorId == kVendorIdQualcomm;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
|
||||
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
|
||||
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
|
||||
//
|
||||
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
|
||||
// content as possible, and a trace sweep of every fixture showed the wider
|
||||
// alternatives all cost more than they fix:
|
||||
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
|
||||
// (its transmittance/accumulate passes disable depth writes themselves) - the
|
||||
// only real content it caught was harmless additive glow effects (Create);
|
||||
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
|
||||
// drawn once per surface, and rely on their depth writes for occlusion;
|
||||
// - separate-alpha accumulation over an over-blending color channel has no
|
||||
// known pairing with a depth-equality chain (color channel only, see tests).
|
||||
// If a future workload pairs another blend shape with an equality chain, widen
|
||||
// this with that evidence in hand rather than pre-emptively.
|
||||
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
|
||||
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
|
||||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
|
||||
if (!payload.depthWriteEnable) {
|
||||
return false;
|
||||
}
|
||||
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
|
||||
// pipeline's interpolated Z, so a driver that varies the vertex position math
|
||||
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
|
||||
// passthrough is the exception that stays exposed; no known content pairs one with
|
||||
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
|
||||
if (payload.fragmentReplacesDepth) {
|
||||
return false;
|
||||
}
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
|
||||
if (attachment.blendEnable != VK_TRUE) {
|
||||
continue;
|
||||
}
|
||||
// All color writes masked: blending is moot (depth-prepass pattern that left
|
||||
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
|
||||
if (attachment.colorWriteMask == 0) {
|
||||
continue;
|
||||
}
|
||||
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
|
||||
// accumulates into a 2-target MRT and must stay stripped.
|
||||
if (IsAccumulationBlend(attachment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PipelineFactory::~PipelineFactory() {
|
||||
DestroyAll();
|
||||
if (m_pipelineCache != VK_NULL_HANDLE) {
|
||||
@@ -152,6 +229,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
if (payload.colorAttachmentCount > 0) {
|
||||
XXHASH_VERIFY(XXH64_update(
|
||||
m_hashState,
|
||||
@@ -165,23 +244,108 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const HashType hash = ComputeHash(payload);
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
return it->second;
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second.pipeline;
|
||||
}
|
||||
|
||||
VkPipeline pipeline = CreatePipeline(payload);
|
||||
m_cache.emplace(hash, pipeline);
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||
m_frameCounter});
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void PipelineFactory::DestroyAll() {
|
||||
for (auto& pair : m_cache) {
|
||||
if (pair.second != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second, nullptr);
|
||||
if (pair.second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
|
||||
}
|
||||
}
|
||||
m_cache.clear();
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::OnFrameBoundary() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
||||
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
|
||||
// pipeline" memo when this returns non-zero: the memo can return a cached
|
||||
// handle without touching this cache, so an evicted pipeline may still be
|
||||
// memoized (present-less flush loops never reset the memo per frame).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
|
||||
m_cache.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (it->second.programHash == programHash) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
|
||||
evicted, static_cast<unsigned long long>(programHash));
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||
@@ -258,6 +422,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
colorAttachments[i] = payload.colorBlendAttachments[i];
|
||||
}
|
||||
// Suppress depth writes on accumulation-blended pipelines when the active driver
|
||||
// cannot keep vertex positions invariant across the pipelines of a multi-pass
|
||||
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
|
||||
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
|
||||
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
|
||||
// This bakes the decision into the pipeline, which only works because depth write is
|
||||
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
|
||||
// would let the record-time value override it and silently disable the quirk.
|
||||
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
|
||||
depthStencil.depthWriteEnable = VK_FALSE;
|
||||
}
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
||||
blend.logicOp = payload.logicOp;
|
||||
|
||||
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
|
||||
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
|
||||
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
|
||||
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
@@ -62,13 +65,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||
void DestroyAll();
|
||||
|
||||
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
// that stamp its factory entry). Returns the number destroyed.
|
||||
Uint32 EvictByProgramHash(HashType programHash);
|
||||
|
||||
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||
// cross-pipeline position invariance that some mobile compilers do not provide, even
|
||||
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
|
||||
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
|
||||
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
|
||||
// only depth-writing shape the chain actually uses - so every other blend
|
||||
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
|
||||
// its depth writes. Set at renderer initialization based on the active driver.
|
||||
static void SetSuppressBlendedDepthWrite(Bool enabled);
|
||||
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
|
||||
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
|
||||
// the known-affected vendor (Qualcomm).
|
||||
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
|
||||
Uint32 vendorId);
|
||||
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
|
||||
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
|
||||
// and payload-only so tests can pin the contract without a VkDevice.
|
||||
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||
|
||||
private:
|
||||
struct PipelineCacheEntry {
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
// The hashed inputs the eviction paths key on: programHash ties the entry to
|
||||
// its ProgramFactory entry, renderPass records the exact handle the hash
|
||||
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
|
||||
HashType programHash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
|
||||
// cache eviction (see OnFrameBoundary).
|
||||
Uint64 lastUsedFrame = 0;
|
||||
};
|
||||
|
||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,16 @@
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8 {
|
||||
Unknown = 0,
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger,
|
||||
};
|
||||
|
||||
class ProgramFactory {
|
||||
public:
|
||||
enum class DescriptorBindingKind : Uint8 {
|
||||
@@ -34,6 +42,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SurfaceRotate90 = 1 << 2,
|
||||
SurfaceRotate180 = 1 << 3,
|
||||
SurfaceRotate270 = 1 << 4,
|
||||
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
|
||||
// Only ever set for a draw whose every sampler binding is clamped to a single mip
|
||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
||||
ExplicitLod0Sampling = 1 << 5,
|
||||
// Decorates the last vertex-processing stage's captured varyings with
|
||||
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
|
||||
// recorded while GL transform feedback is active, so plain draws keep the
|
||||
// undecorated variant.
|
||||
XfbCapture = 1 << 6,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
@@ -51,11 +69,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<DescriptorBindingKind> bindingKinds;
|
||||
Vector<Uint32> dynamicBindings;
|
||||
Vector<Int> uniformBlockIndexByBinding;
|
||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||
// binding with descriptorCount = N).
|
||||
Vector<Uint16> bindingDescriptorCounts;
|
||||
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
||||
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
||||
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
|
||||
Vector<String> samplerNameByBinding;
|
||||
Vector<Int> samplerUniformLocationByBinding;
|
||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||
Vector<VkFormat> storageImageFormatByBinding;
|
||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||
Vector<String> storageBlockNameByBinding;
|
||||
Vector<Int> storageBlockIndexByBinding;
|
||||
// Set once during ReflectLayout so the per-draw path can skip the whole
|
||||
// storage-image preparation for the overwhelming majority of programs.
|
||||
Bool hasStorageImages = false;
|
||||
Int globalUboBinding = -1;
|
||||
Uint32 activeVertexInputLocationMask = 0;
|
||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||
@@ -64,6 +94,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
||||
Uint32 producerOutputComponentCount = 0;
|
||||
Uint32 fragmentInputComponentCount = 0;
|
||||
// The fragment module declares the DepthReplacing execution mode (writes
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary).
|
||||
Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
|
||||
@@ -79,11 +116,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
dynamicBindings = std::move(other.dynamicBindings);
|
||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
|
||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
||||
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
|
||||
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
|
||||
storageImageUsesBindingFormatByBinding =
|
||||
std::move(other.storageImageUsesBindingFormatByBinding);
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -92,15 +136,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
if (this == &other) {
|
||||
@@ -115,11 +164,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
dynamicBindings = std::move(other.dynamicBindings);
|
||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
|
||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
||||
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
|
||||
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
|
||||
storageImageUsesBindingFormatByBinding =
|
||||
std::move(other.storageImageUsesBindingFormatByBinding);
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -128,15 +184,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -166,10 +227,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
|
||||
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
|
||||
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
|
||||
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
|
||||
// layout handle value may be recycled for an unrelated layout, and the program
|
||||
// hash may be re-inserted by a later rebuild of the same content.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false)
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
@@ -179,7 +254,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||
// (their command buffers retired many frames ago), mirroring
|
||||
// VkRenderPassManager::OnPresent's sweep.
|
||||
void OnFrameBoundary();
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
// stripped conservatively and forfeits its depth write.
|
||||
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
|
||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -206,7 +298,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
|
||||
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
|
||||
Bool m_shaderDrawParametersEnabled = false;
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -247,6 +247,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
|
||||
m_extent = createInfo.imageExtent;
|
||||
// The surface-space extent this swapchain was built from, i.e. before the
|
||||
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
|
||||
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
|
||||
// and makes the comparison alternate forever.
|
||||
m_surfaceExtent = defaultFramebufferExtent;
|
||||
m_preTransform = createInfo.preTransform;
|
||||
|
||||
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
|
||||
@@ -257,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_images.resize(imageCount, VK_NULL_HANDLE);
|
||||
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
||||
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -428,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_images.clear();
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
return m_imageContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
m_imageContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
return m_depthStencilContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
m_depthStencilContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
void SwapchainObject::SetAllDepthStencilContentUndefined() {
|
||||
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
|
||||
m_depthStencilContentDefined[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
VkImage SwapchainObject::GetImage(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
||||
return m_images[index];
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
// created from - the value to compare a freshly queried currentExtent against.
|
||||
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
|
||||
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
|
||||
const Vector<VkImage>& GetImages() const { return m_images; }
|
||||
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
|
||||
@@ -49,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
||||
SizeT GetImageCount() const { return m_images.size(); }
|
||||
|
||||
// EGL content-validity tracking for the default framebuffer. A color
|
||||
// buffer's content is undefined once its image has been presented
|
||||
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
|
||||
// and every ancillary (depth/stencil) buffer's content is undefined
|
||||
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
|
||||
// render-pass manager turns an undefined attachment's tile load into
|
||||
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
|
||||
// garbage) and a render pass storing into an attachment sets it back
|
||||
// to defined.
|
||||
Bool IsImageContentDefined(Uint32 index) const;
|
||||
void SetImageContentDefined(Uint32 index, Bool defined);
|
||||
Bool IsDepthStencilContentDefined(Uint32 index) const;
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -63,6 +81,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
Vector<VkImage> m_images;
|
||||
Vector<VkImageView> m_imageViews;
|
||||
@@ -73,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
||||
Vector<VkImageView> m_depthStencilImageViews;
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -78,6 +80,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return uniformUnit >= 0 ? uniformUnit : 0;
|
||||
}
|
||||
|
||||
VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat) {
|
||||
if (useBindingFormat) {
|
||||
const TextureInternalFormat bindingInternalFormat =
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat);
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat);
|
||||
}
|
||||
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
|
||||
}
|
||||
|
||||
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
@@ -193,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
||||
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
m_hasLastDescriptor = false;
|
||||
m_lastBindValid = false;
|
||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||
for (auto& memo : m_samplerResolveMemo) {
|
||||
@@ -200,6 +213,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
||||
SizeT purgedSets = 0;
|
||||
for (auto& frame : m_frames) {
|
||||
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
|
||||
if (it == frame.descriptorSetCacheByLayout.end()) {
|
||||
continue;
|
||||
}
|
||||
// Free the sets back to their pools and credit the bucket accounting, so
|
||||
// program churn recycles pool capacity instead of abandoning the slots.
|
||||
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
|
||||
// in-flight command buffer references these sets.
|
||||
for (const auto& cached : it->second.sets) {
|
||||
if (cached.set == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
|
||||
const auto bucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
|
||||
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
|
||||
--bucket->allocatedSets;
|
||||
}
|
||||
}
|
||||
purgedSets += it->second.sets.size();
|
||||
frame.descriptorSetCacheByLayout.erase(it);
|
||||
}
|
||||
if (purgedSets > 0) {
|
||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
||||
m_hasLastDescriptor = false;
|
||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||
}
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -276,6 +323,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
|
||||
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
|
||||
}
|
||||
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
|
||||
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
|
||||
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
|
||||
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
|
||||
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
|
||||
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
|
||||
// descriptor valid.
|
||||
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
|
||||
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
||||
SamplerResolveMemo* viewFormatMemo =
|
||||
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
|
||||
VkFormat sampledViewFormat;
|
||||
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
|
||||
viewFormatMemo->viewFormatSource == resource->format &&
|
||||
viewFormatMemo->viewFormatDomain == numericDomain) {
|
||||
sampledViewFormat = viewFormatMemo->viewFormat;
|
||||
} else {
|
||||
sampledViewFormat =
|
||||
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
|
||||
if (viewFormatMemo != nullptr) {
|
||||
viewFormatMemo->viewFormatSource = resource->format;
|
||||
viewFormatMemo->viewFormatDomain = numericDomain;
|
||||
viewFormatMemo->viewFormat = sampledViewFormat;
|
||||
viewFormatMemo->viewFormatValid = true;
|
||||
}
|
||||
}
|
||||
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||
return false;
|
||||
}
|
||||
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
|
||||
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
|
||||
const VkImageView sampledImageView =
|
||||
sampledViewFormat == resource->format
|
||||
? resource->sampledView
|
||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||
if (sampledImageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
||||
static_cast<Int>(numericDomain));
|
||||
return false;
|
||||
}
|
||||
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
|
||||
// sampler object and texture (both by lifetime id + version) are unchanged from the
|
||||
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler
|
||||
@@ -290,24 +386,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint16 samplerVersion = samplerToUse->GetVersion();
|
||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
|
||||
// follows uploads as well as GL parameters - so it belongs in the memo key too.
|
||||
const Uint32 viewLevelCount = resource->sampledLevelCount;
|
||||
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) {
|
||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
||||
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
|
||||
resolvedSampler = memo.sampler;
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
|
||||
forceNearestFiltering, viewLevelCount);
|
||||
memo.samplerLifetimeId = samplerLifetimeId;
|
||||
memo.samplerVersion = samplerVersion;
|
||||
memo.textureLifetimeId = textureLifetimeId;
|
||||
memo.textureParamsVersion = textureParamsVersion;
|
||||
memo.forceNearestFiltering = forceNearestFiltering;
|
||||
memo.viewLevelCount = viewLevelCount;
|
||||
memo.sampler = resolvedSampler;
|
||||
memo.valid = true;
|
||||
}
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
||||
resource->sampledLevelCount);
|
||||
}
|
||||
outImageInfo = {
|
||||
.sampler = resolvedSampler,
|
||||
.imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
|
||||
.imageView = sampledImageView,
|
||||
.imageLayout = resource->layout,
|
||||
};
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
@@ -344,6 +448,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
||||
Bool sawSampler = false;
|
||||
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||
continue;
|
||||
}
|
||||
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
||||
if (texture == nullptr) return false;
|
||||
const auto& levelRange = texture->GetLevelRange();
|
||||
if (levelRange.x() != levelRange.y()) return false;
|
||||
|
||||
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
|
||||
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
||||
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (effectiveSampler == nullptr) return false;
|
||||
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
|
||||
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
|
||||
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
|
||||
// min/mag decision. That only matches the implicit form when lambda could not have been
|
||||
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
|
||||
// filters are the same and the choice cannot be observed.
|
||||
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
|
||||
? 0.0f
|
||||
: effectiveSampler->GetMaxLod();
|
||||
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
|
||||
return false;
|
||||
}
|
||||
sawSampler = true;
|
||||
}
|
||||
return sawSampler;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
@@ -360,6 +506,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
// default has no image it is unsampleable; report it as "unbound" so callers keep
|
||||
// taking their fallback paths instead of trying to sync a storage-less texture.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
|
||||
outTexture.reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -380,7 +532,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
|
||||
// touching the refcount (no atomic inc/dec per binding per draw).
|
||||
return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
// default has no image it is unsampleable; report it as "unbound" so the caller
|
||||
// substitutes its fallback texture exactly like it did for the old null slot.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||
return nullptr;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
@@ -558,9 +718,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
|
||||
VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel);
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
|
||||
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
|
||||
binding);
|
||||
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
|
||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
||||
useBindingFormat ? "true" : "false");
|
||||
return false;
|
||||
}
|
||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
view = resource->fullView;
|
||||
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
||||
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
||||
static_cast<Int>(viewFormat),
|
||||
useBindingFormat ? "true" : "false");
|
||||
return false;
|
||||
}
|
||||
outImageInfo.sampler = VK_NULL_HANDLE;
|
||||
outImageInfo.imageView = view;
|
||||
@@ -599,7 +782,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
||||
if (!texture) {
|
||||
continue;
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
continue;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
}
|
||||
|
||||
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
||||
@@ -610,9 +801,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::CollectStorageImageTextures(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
|
||||
outTextures.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
"CollectStorageImageTextures: GL context is null");
|
||||
|
||||
const Uint32 bindingCount =
|
||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||
continue;
|
||||
}
|
||||
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (location < 0) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u",
|
||||
imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u",
|
||||
imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
|
||||
outTextures.push_back(texture);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
UboBindResult& out) const {
|
||||
Uint32 arrayElement, UboBindResult& out) const {
|
||||
const void* outData = nullptr;
|
||||
VkDeviceSize outSize = 0;
|
||||
|
||||
@@ -638,7 +873,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
|
||||
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
|
||||
const Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
|
||||
Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
|
||||
if (arrayElement > 0) {
|
||||
const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding);
|
||||
const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() &&
|
||||
arrayElement < arrayIt->second.size();
|
||||
MOBILEGL_ASSERT(elementValid,
|
||||
"ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding,
|
||||
arrayElement);
|
||||
if (!elementValid) {
|
||||
return false;
|
||||
}
|
||||
blockIndex = arrayIt->second[arrayElement];
|
||||
}
|
||||
MOBILEGL_ASSERT(blockIndex >= 0,
|
||||
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
|
||||
|
||||
@@ -746,6 +993,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
|
||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||
// cache grows - never on the per-draw reuse path.
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = maxSets;
|
||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
@@ -825,7 +1077,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
||||
if (cache.cursor < cache.sets.size()) {
|
||||
outDescriptorSet = cache.sets[cache.cursor++];
|
||||
outDescriptorSet = cache.sets[cache.cursor++].set;
|
||||
} else {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
@@ -839,7 +1091,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return allocResult;
|
||||
}
|
||||
|
||||
cache.sets.push_back(outDescriptorSet);
|
||||
// The successful allocation came from the bucket the alloc helper left
|
||||
// active; record it so a layout-destroyed purge can free the set back.
|
||||
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
|
||||
++cache.cursor;
|
||||
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
||||
cache.sets.size());
|
||||
@@ -885,11 +1139,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfos.clear();
|
||||
texelBufferViews.clear();
|
||||
dynamicOffsets.clear();
|
||||
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
||||
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
|
||||
Uint32 uboArrayExtra = 0;
|
||||
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
||||
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
||||
}
|
||||
writes.reserve(m_maxBindings);
|
||||
bufferInfos.reserve(m_maxBindings);
|
||||
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
|
||||
imageInfos.reserve(m_maxBindings);
|
||||
texelBufferViews.reserve(m_maxBindings);
|
||||
dynamicOffsets.reserve(programObj.dynamicBindings.size());
|
||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||
|
||||
const Uint32 bindingCount =
|
||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||
@@ -907,40 +1167,82 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
write.descriptorCount = 1;
|
||||
|
||||
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
|
||||
UboBindResult ubo{};
|
||||
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo);
|
||||
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u",
|
||||
binding);
|
||||
const Uint32 descriptorCount =
|
||||
binding < programObj.bindingDescriptorCounts.size()
|
||||
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
||||
: 1u;
|
||||
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
UboBindResult ubo{};
|
||||
const Bool hasPayload =
|
||||
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
|
||||
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
|
||||
binding, element);
|
||||
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
||||
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
||||
bufferInfo.offset = 0;
|
||||
Uint32 dynOffset;
|
||||
if (ubo.directBindable) {
|
||||
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
||||
bufferInfo.buffer = ubo.buffer;
|
||||
bufferInfo.range = ubo.range;
|
||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||
} else {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
|
||||
binding);
|
||||
return false;
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
||||
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
||||
bufferInfo.offset = 0;
|
||||
Uint32 dynOffset;
|
||||
if (ubo.directBindable) {
|
||||
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
||||
bufferInfo.buffer = ubo.buffer;
|
||||
bufferInfo.range = ubo.range;
|
||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||
} else {
|
||||
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
||||
// uniform bytes re-use the slice already uploaded this frame.
|
||||
const Bool isGlobalUbo =
|
||||
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
|
||||
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
||||
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 uboContentVersion = program.GetUBOContentVersion();
|
||||
Bool reusedSlice = false;
|
||||
if (isGlobalUbo) {
|
||||
for (const auto& memo : m_globalUboMemo) {
|
||||
if (memo.buffer != VK_NULL_HANDLE &&
|
||||
memo.programLifetimeId == uboProgramLifetimeId &&
|
||||
memo.frameSerial == uboFrameSerial &&
|
||||
memo.uboContentVersion == uboContentVersion &&
|
||||
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
|
||||
bufferInfo.buffer = memo.buffer;
|
||||
bufferInfo.range = memo.range;
|
||||
dynOffset = static_cast<Uint32>(memo.offset);
|
||||
reusedSlice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reusedSlice) {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
|
||||
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
||||
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
// Dynamic offsets are consumed in binding order, then array element order,
|
||||
// matching Vulkan's dynamic-offset consumption rules.
|
||||
dynamicOffsets.push_back(dynOffset);
|
||||
}
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||
write.pBufferInfo = &bufferInfos.back();
|
||||
write.descriptorCount = descriptorCount;
|
||||
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
|
||||
writes.push_back(write);
|
||||
dynamicOffsets.push_back(dynOffset);
|
||||
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||
@@ -1068,8 +1370,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_hasLastDescriptor = cacheable;
|
||||
}
|
||||
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
|
||||
// Skip the driver call when this exact binding is already live on the
|
||||
// command buffer (see the bind-dedup shadow in the header).
|
||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
||||
if (identicalBind) {
|
||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
||||
identicalBind = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!identicalBind) {
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, offsetCount, dynamicOffsets.data());
|
||||
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
||||
m_lastBindValid = true;
|
||||
m_lastBindSet = descriptorSet;
|
||||
m_lastBindLayout = programObj.pipelineLayout;
|
||||
m_lastBindPoint = bindPoint;
|
||||
m_lastBindOffsetCount = offsetCount;
|
||||
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
||||
} else {
|
||||
m_lastBindValid = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -39,9 +39,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
// vkFreeDescriptorSets'd back to their pools (created with
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -49,6 +66,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
// format and never silently fall back to the backing image format.
|
||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat);
|
||||
|
||||
// True when the program reads at least one sampler and every one of them is bound to a
|
||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
private:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
@@ -56,8 +89,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 allocatedSets = 0;
|
||||
};
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
// owning bucket's accounting.
|
||||
struct CachedDescriptorSet {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct DescriptorSetCacheEntry {
|
||||
Vector<VkDescriptorSet> sets;
|
||||
Vector<CachedDescriptorSet> sets;
|
||||
Uint32 cursor = 0;
|
||||
};
|
||||
|
||||
@@ -107,7 +148,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
UboBindResult& out) const;
|
||||
Uint32 arrayElement, UboBindResult& out) const;
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
@@ -147,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_lastDescriptorSignature = 0;
|
||||
Bool m_hasLastDescriptor = false;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
|
||||
// layout+bind point, so a pipeline-layout switch always rebinds.
|
||||
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
|
||||
Bool m_lastBindValid = false;
|
||||
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
|
||||
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
Uint32 m_lastBindOffsetCount = 0;
|
||||
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
|
||||
|
||||
// Global-UBO transient-slice reuse: MC leaves the default uniform block
|
||||
// untouched across long GUI/terrain runs, so the per-draw re-upload of
|
||||
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
|
||||
// serial guards arena recycling; the content version guards writes).
|
||||
struct GlobalUboSliceMemo {
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint64 frameSerial = 0;
|
||||
Uint32 uboContentVersion = 0;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
VkDeviceSize range = 0;
|
||||
};
|
||||
static constexpr Uint32 kGlobalUboMemoSize = 4;
|
||||
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
|
||||
Uint32 m_globalUboMemoNext = 0;
|
||||
|
||||
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
|
||||
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
|
||||
// sampler objects with identical state still resolve to one VkSampler. This memo only
|
||||
@@ -163,11 +233,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
Uint32 viewLevelCount = 0;
|
||||
Uint16 samplerVersion = 0;
|
||||
Uint16 textureParamsVersion = 0;
|
||||
Bool forceNearestFiltering = false;
|
||||
Bool valid = false;
|
||||
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
|
||||
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
|
||||
// so a reinterpreted sampler pays that scan once, not once per draw.
|
||||
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
|
||||
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
Bool viewFormatValid = false;
|
||||
};
|
||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
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
|
||||
// buffer's reused address can alias an old cache entry, but only under a
|
||||
// byte-identical attribute layout - and the entry payload is a pure function
|
||||
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
||||
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
||||
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
||||
// aging sweep bounds that.
|
||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
@@ -51,14 +58,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
const void* memoState = nullptr;
|
||||
Uint64 memoEpoch = 0;
|
||||
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
|
||||
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
|
||||
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *entry;
|
||||
}
|
||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||
return entry;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
return it->second;
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
@@ -66,6 +86,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
@@ -74,8 +95,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -83,6 +105,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat vkFormat = sourceVkFormat;
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
if (!SupportsVertexBufferFormat(vkFormat)) {
|
||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
|
||||
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
@@ -92,8 +141,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint32 stride =
|
||||
const Uint32 sourceStride =
|
||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
// For a client-memory array attr.Offset holds the raw client pointer, and the
|
||||
// draw path re-uploads the data to a 16-aligned transient slice with attribute
|
||||
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
|
||||
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
|
||||
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
|
||||
((sourceStride % requiredAlignment) != 0 ||
|
||||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
|
||||
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
|
||||
@@ -103,20 +177,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
|
||||
bindingAttributeLocations.push_back(location);
|
||||
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
}
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& entry = m_cache[hash];
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
||||
entry.bindingConversions = std::move(bindingConversions);
|
||||
entry.unsupportedAttribMask = unsupportedAttribMask;
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
@@ -124,6 +227,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return entry;
|
||||
}
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||
// reference into the current entry across its setup, and unordered_map
|
||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||
// from the VAO state (same hash, same content).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert.
|
||||
++m_evictionEpoch;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra) {
|
||||
if (isBgra) {
|
||||
@@ -282,4 +412,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const SizeT componentSize = GetComponentSize(type);
|
||||
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
|
||||
}
|
||||
|
||||
Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8_USCALED:
|
||||
case VK_FORMAT_R8_SSCALED:
|
||||
case VK_FORMAT_R8G8_USCALED:
|
||||
case VK_FORMAT_R8G8_SSCALED:
|
||||
case VK_FORMAT_R8G8B8_USCALED:
|
||||
case VK_FORMAT_R8G8B8_SSCALED:
|
||||
case VK_FORMAT_R8G8B8A8_USCALED:
|
||||
case VK_FORMAT_R8G8B8A8_SSCALED:
|
||||
case VK_FORMAT_R16_USCALED:
|
||||
case VK_FORMAT_R16_SSCALED:
|
||||
case VK_FORMAT_R16G16_USCALED:
|
||||
case VK_FORMAT_R16G16_SSCALED:
|
||||
case VK_FORMAT_R16G16B16_USCALED:
|
||||
case VK_FORMAT_R16G16B16_SSCALED:
|
||||
case VK_FORMAT_R16G16B16A16_USCALED:
|
||||
case VK_FORMAT_R16G16B16A16_SSCALED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) {
|
||||
switch (componentCount) {
|
||||
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||
case 2: return VK_FORMAT_R32G32_SFLOAT;
|
||||
case 3: return VK_FORMAT_R32G32B32_SFLOAT;
|
||||
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
default: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const {
|
||||
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -19,25 +19,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
enum class VertexStreamConversion : Uint8 {
|
||||
None = 0,
|
||||
Repack,
|
||||
ScaledIntegerToFloat32,
|
||||
};
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
||||
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
||||
// pipelines on that minted one VkPipeline per chunk section for an
|
||||
// identical layout, defeating pipeline reuse and the per-draw memo.
|
||||
// Pipelines depend only on the layout, so they key on this instead.
|
||||
HashType layoutHash = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
||||
// a const entry reference.
|
||||
mutable Uint64 lastUsedFrameBoundary = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
Vector<VkVertexInputAttributeDescription> attributes;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
|
||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
};
|
||||
|
||||
explicit VertexInputStateFactory(const VulkanRendererConfig& config):
|
||||
m_config(config) {}
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
~VertexInputStateFactory() = default;
|
||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||
|
||||
@@ -48,6 +70,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
||||
// minting fresh keys; without eviction the map grows for the whole session.
|
||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||
// and the draw path's entry reference never spans a frame boundary, so
|
||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||
// compare except on sweep boundaries.
|
||||
void OnFrameBoundary();
|
||||
static SizeT GetComponentSize(DataType type);
|
||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
@@ -56,9 +86,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
|
||||
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
||||
// so INSERT invalidates references to stored values. The draw path (and
|
||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
||||
// only the unique_ptr cell moves, never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
||||
// their heap-allocated entry (stable across map insert/rehash by
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
|
||||
// The app writes into the persistent map with no explicit flush, so its memory must
|
||||
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
|
||||
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
|
||||
@@ -141,6 +145,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_transientUploadArena.BeginFrame(frameIndex);
|
||||
}
|
||||
|
||||
void VkBufferManager::CollectAllDeferredReleases() {
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||
CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void VkBufferManager::NotifyDeviceIdle() {
|
||||
// Everything submitted so far has completed. Work recorded for the
|
||||
// current frame has not been submitted yet, so the current serial
|
||||
@@ -197,16 +210,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
}
|
||||
|
||||
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
|
||||
VkBufferResource* VkBufferManager::GetOrCreateResource(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||
auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
|
||||
// Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's
|
||||
// backend-resource SharedPtr (already set, or set below), so callers that only dereference
|
||||
// it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind.
|
||||
const auto& existing = bufferObject->GetBackendResource();
|
||||
if (existing) {
|
||||
return existing;
|
||||
return static_cast<VkBufferResource*>(existing.get());
|
||||
}
|
||||
auto resource = MakeShared<VkBufferResource>();
|
||||
VkBufferResource* raw = resource.get();
|
||||
bufferObject->SetBackendResource(resource);
|
||||
TrackLiveResource(resource);
|
||||
return resource;
|
||||
return raw;
|
||||
}
|
||||
|
||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
||||
@@ -452,7 +469,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it from the current shadow - MappedData() is still the shadow here because the
|
||||
// frontend adopts (and drops) the shadow only after this returns.
|
||||
DeferRelease(std::move(resource->buffer));
|
||||
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
|
||||
const VkBufferUsageFlags persistentUsage =
|
||||
kPersistentBackedUsage |
|
||||
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
|
||||
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
|
||||
resource->persistentMapped = false;
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
||||
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
Bool transientPersistentMapping = false;
|
||||
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
|
||||
// carries the transform feedback usage so capture targets can bind directly.
|
||||
Bool transformFeedbackUsageEnabled = false;
|
||||
};
|
||||
|
||||
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
|
||||
@@ -77,6 +80,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||
// transient arena's parked superseded blocks). Only valid when the
|
||||
// caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||
void NotifyDeviceIdle();
|
||||
// A frame slot's submission fence has been waited: every serial up to
|
||||
@@ -124,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
private:
|
||||
Bool InitializeTransientArenas();
|
||||
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
|
||||
SharedPtr<VkBufferResource> GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
|
||||
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
|
||||
VkMemoryPropertyFlags requiredFlags = 0);
|
||||
|
||||
@@ -157,6 +157,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
|
||||
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
|
||||
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
|
||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
|
||||
|
||||
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
|
||||
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
|
||||
if (resolvedSize == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* Map();
|
||||
void Unmap();
|
||||
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
|
||||
Bool Invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
|
||||
|
||||
VkBuffer GetHandle() const { return m_buffer; }
|
||||
VkDeviceSize GetSize() const { return m_size; }
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -127,6 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_pendingClears.erase(key);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
@@ -221,6 +223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -238,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -245,6 +249,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -260,6 +268,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||
@@ -287,6 +298,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!LockTextureLocked(key, outTexture)) {
|
||||
@@ -325,6 +339,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -345,6 +362,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -361,6 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it != m_pendingClears.end()) {
|
||||
m_pendingClears.erase(it);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -120,7 +121,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
public:
|
||||
// Lock-free probe for the consecutive-draw fast path: any pending clear
|
||||
// forces the full SetupDraw path (which materializes/consumes it).
|
||||
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
|
||||
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
|
||||
// read it before taking the lock: during draw batches the pending set
|
||||
// is almost always empty, so this turns several locked map probes per
|
||||
// draw into one relaxed load.
|
||||
std::atomic<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
|
||||
@@ -16,31 +16,21 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
|
||||
case 1:
|
||||
// GL promises "at least the requested samples", so a non-power-of-two
|
||||
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||
if (requestedSamples <= 1) {
|
||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
return true;
|
||||
case 2:
|
||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||
return true;
|
||||
case 4:
|
||||
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
|
||||
return true;
|
||||
case 8:
|
||||
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
|
||||
return true;
|
||||
case 16:
|
||||
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
|
||||
return true;
|
||||
case 32:
|
||||
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
|
||||
return true;
|
||||
case 64:
|
||||
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
|
||||
return true;
|
||||
default:
|
||||
}
|
||||
if (requestedSamples > 64) {
|
||||
return false;
|
||||
}
|
||||
Uint32 bit = 1;
|
||||
while (bit < static_cast<Uint32>(requestedSamples)) {
|
||||
bit <<= 1;
|
||||
}
|
||||
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
|
||||
@@ -166,6 +156,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, view, nullptr);
|
||||
}
|
||||
if (unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, unormTwinView, nullptr);
|
||||
}
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(allocator, image, allocation);
|
||||
}
|
||||
@@ -173,6 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
image = VK_NULL_HANDLE;
|
||||
allocation = nullptr;
|
||||
view = VK_NULL_HANDLE;
|
||||
unormTwinView = VK_NULL_HANDLE;
|
||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
format = VK_FORMAT_UNDEFINED;
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -180,6 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
internalFormat = TextureInternalFormat::Unknown;
|
||||
samples = 0;
|
||||
deadSinceFrame = kNeverObservedDead;
|
||||
}
|
||||
|
||||
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
||||
@@ -206,6 +201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
}
|
||||
m_renderbufferResources.clear();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||
m_pendingRenderbufferClears.clear();
|
||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||
s_activeRenderPass = {};
|
||||
@@ -213,22 +209,80 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
||||
Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
|
||||
deadRenderbuffers.reserve(m_renderbufferResources.size());
|
||||
for (auto& [renderbuffer, resource] : m_renderbufferResources) {
|
||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
||||
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
|
||||
deadRenderbuffers.emplace_back(renderbuffer);
|
||||
}
|
||||
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
||||
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
||||
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
||||
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
||||
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
||||
// 1024-frame retirement would pin it for no additional safety).
|
||||
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
||||
}
|
||||
|
||||
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
|
||||
// The superseded backing may still be referenced by in-flight command buffers
|
||||
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
|
||||
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
|
||||
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
for (auto* renderbuffer : deadRenderbuffers) {
|
||||
auto resourceIt = m_renderbufferResources.find(renderbuffer);
|
||||
if (resourceIt != m_renderbufferResources.end()) {
|
||||
resourceIt->second.Destroy(m_device, m_allocator);
|
||||
m_renderbufferResources.erase(resourceIt);
|
||||
m_deferredRenderbufferReleases.push_back(
|
||||
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
resource.view = VK_NULL_HANDLE;
|
||||
resource.unormTwinView = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
||||
if (m_deferredRenderbufferReleases.empty()) {
|
||||
return;
|
||||
}
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
|
||||
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
|
||||
return false;
|
||||
}
|
||||
m_pendingRenderbufferClears.erase(renderbuffer);
|
||||
if (release.view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.view, nullptr);
|
||||
}
|
||||
if (release.unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
|
||||
}
|
||||
if (release.image != VK_NULL_HANDLE) {
|
||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
||||
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
|
||||
// command buffers submitted up to frames-in-flight frames ago (it was legally
|
||||
// attached and drawn right up to its deletion), so the first observation of an
|
||||
// expired weak reference only stamps the current frame counter; Destroy runs once
|
||||
// enough frame boundaries have passed that the stamping frame's submission fence
|
||||
// has provably been waited (see RetireAgeFrames).
|
||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
||||
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
|
||||
auto& resource = it->second;
|
||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
||||
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
|
||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
|
||||
resource.deadSinceFrame = m_frameCounter;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
m_pendingRenderbufferClears.erase(it->first);
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
it = m_renderbufferResources.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,12 +303,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet",
|
||||
renderbuffer->GetExternalIndex());
|
||||
return nullptr;
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization.
|
||||
const VkImageUsageFlags imageUsage =
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
|
||||
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
|
||||
// GL allows the implementation to allocate more samples than requested
|
||||
// (glRenderbufferStorageMultisample only promises "at least"), and devices
|
||||
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
|
||||
// nearest supported count for this format.
|
||||
if (renderbuffer->GetSamples() > 0) {
|
||||
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
|
||||
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
|
||||
VkImageFormatProperties formatProperties{};
|
||||
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
|
||||
&formatProperties) == VK_SUCCESS) {
|
||||
supported = formatProperties.sampleCounts;
|
||||
}
|
||||
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
|
||||
}
|
||||
const VkSampleCountFlags supported = supportedIt->second;
|
||||
if ((supported & sampleCount) == 0) {
|
||||
// Smallest supported count above the request, else the largest below it.
|
||||
Uint32 rounded = 0;
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rounded == 0) {
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rounded != 0) {
|
||||
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||
@@ -268,9 +404,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.samples != renderbuffer->GetSamples();
|
||||
if (!needsCreate) {
|
||||
resource.renderbuffer = renderbuffer;
|
||||
// A new renderbuffer at a recycled address may adopt a compatible entry that
|
||||
// was already stamped dead; it is alive again, so cancel the aging.
|
||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
||||
return &resource;
|
||||
}
|
||||
|
||||
// Respecify: park the old backing for aged destruction instead of destroying
|
||||
// inline - it may still be referenced by in-flight command buffers.
|
||||
DeferRenderbufferBackingRelease(resource);
|
||||
resource.Destroy(m_device, m_allocator);
|
||||
resource.renderbuffer = renderbuffer;
|
||||
|
||||
@@ -285,9 +427,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.format = format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
imageInfo.usage = imageUsage;
|
||||
imageInfo.samples = sampleCount;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
|
||||
// is disabled, which needs a format-reinterpreting second view.
|
||||
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
|
||||
if (hasUnormTwin) {
|
||||
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
@@ -322,6 +470,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
||||
"vkCreateImageView(renderbuffer)");
|
||||
if (hasUnormTwin) {
|
||||
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
|
||||
"vkCreateImageView(renderbuffer unorm twin)");
|
||||
}
|
||||
|
||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
resource.format = format;
|
||||
@@ -386,6 +539,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkRenderPassManager::QueueRenderbufferClear(
|
||||
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo) {
|
||||
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
|
||||
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
|
||||
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
QueueRenderbufferClear(
|
||||
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
|
||||
drawFbo.GetAttachment(attachmentType));
|
||||
}
|
||||
}
|
||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
QueueRenderbufferClear(
|
||||
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
|
||||
@@ -406,12 +571,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||
Bool includeDefaultFboDepthStencil) {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
}
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
auto readBuffer = fbo.GetReadBuffer();
|
||||
@@ -485,9 +656,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachment <= FramebufferAttachmentType::BackRight);
|
||||
if (isDefaultColorAttachment) {
|
||||
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// Content validity feeds the attachment's loadOp (see the
|
||||
// creation path), so it must key the cache as well.
|
||||
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
} else if (attachment == FramebufferAttachmentType::Depth ||
|
||||
attachment == FramebufferAttachmentType::Stencil) {
|
||||
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
||||
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
@@ -542,14 +721,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
combineFramebufferAttachmentObjHash(drawbuf);
|
||||
}
|
||||
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||
// entirely, so it must hash differently from the depth-full flavor.
|
||||
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
if (depthStencilIncluded) {
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex) {
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
// depth attachment when the caller needs it, when a depth/stencil clear is
|
||||
// pending, or when the active pass already carries it (escalate-only, so
|
||||
// alternating depth-less draws never split an established depth pass).
|
||||
Bool includeDefaultFboDepthStencil = true;
|
||||
if (fbo.IsDefaultFramebuffer()) {
|
||||
Bool activeDefaultHasDepthStencil = false;
|
||||
if (const auto* active = GetActiveRenderPass()) {
|
||||
Bool activeIsSwapchainPass = false;
|
||||
Bool activeHasSwapchainDepthStencil = false;
|
||||
for (const auto& tracked : active->trackedAttachmentLayouts) {
|
||||
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
|
||||
activeHasSwapchainDepthStencil |=
|
||||
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
|
||||
}
|
||||
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
|
||||
}
|
||||
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const Bool pendingDepthStencilClear =
|
||||
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultDepthAtt) ||
|
||||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultStencilAtt);
|
||||
includeDefaultFboDepthStencil =
|
||||
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
|
||||
}
|
||||
|
||||
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
|
||||
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
for (auto attachment : drawBuffers) {
|
||||
@@ -599,14 +813,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
|
||||
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
|
||||
if (activeRenderPass != nullptr &&
|
||||
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
||||
!hasPendingClearOnFramebuffer()) {
|
||||
@@ -623,12 +839,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end())
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
// Color attachment
|
||||
@@ -678,6 +898,88 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// assuming default FBO has the right param
|
||||
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
|
||||
auto drawbuf = drawbufs[i];
|
||||
|
||||
// Renderbuffer color attachments mirror the texture path below, with the
|
||||
// resource (image/view/format/layout) coming from the render-pass manager's
|
||||
// renderbuffer store instead of the texture manager.
|
||||
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
|
||||
const auto& rbAtt = fbo.GetAttachment(drawbuf);
|
||||
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
|
||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||
attachmentDescriptions.emplace_back();
|
||||
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
|
||||
|
||||
ClearAttachmentPayload rbClearPayload{};
|
||||
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
|
||||
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
|
||||
if (rbHasClear &&
|
||||
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
|
||||
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||
rbClearPayload.color =
|
||||
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
|
||||
rbClearPayload.color.z(), 1.0f);
|
||||
}
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
rbDesc.format = rbAttachmentFormat;
|
||||
rbDesc.samples = rbResource->sampleCount;
|
||||
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
||||
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
||||
: VK_ATTACHMENT_LOAD_OP_LOAD);
|
||||
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
|
||||
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
|
||||
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
|
||||
static_cast<Int>(renderbuffer->GetExternalIndex()));
|
||||
|
||||
if (rbHasClear) {
|
||||
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
||||
.attachmentIndex = rbAttachmentIndex,
|
||||
.colorAttachmentSlot = i,
|
||||
.renderbuffer = renderbuffer.get(),
|
||||
.hasInlinePayload = true,
|
||||
.inlinePayload = rbClearPayload,
|
||||
});
|
||||
}
|
||||
|
||||
if (width == 0)
|
||||
width = static_cast<Int>(rbResource->extent.width);
|
||||
if (height == 0)
|
||||
height = static_cast<Int>(rbResource->extent.height);
|
||||
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Renderbuffer,
|
||||
.renderbuffer = renderbuffer,
|
||||
.finalLayout = rbDesc.finalLayout,
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
|
||||
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
|
||||
if (texture == nullptr)
|
||||
continue;
|
||||
@@ -696,6 +998,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
case TextureTarget::TextureRectangle: {
|
||||
desc.flags = 0;
|
||||
desc.format = isDefaultFbo ?
|
||||
@@ -718,6 +1024,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (hasClear) {
|
||||
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
||||
.attachmentIndex = attachmentIndex,
|
||||
.colorAttachmentSlot = i,
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
@@ -733,6 +1040,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
||||
"GetOrCreateRenderPass: swapchain image index out of range");
|
||||
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// EGL: a presented color buffer's content is undefined when its
|
||||
// image comes back around (EGL_BUFFER_DESTROYED, the default
|
||||
// swap behaviour) - skip the tile load instead of reloading
|
||||
// stale pixels nobody may rely on.
|
||||
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::SwapchainColor,
|
||||
.swapchainImageIndex = swapchainImageIndex,
|
||||
@@ -746,12 +1060,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = textureResource->format;
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = att.GetTexture(),
|
||||
.textureRaw = att.GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = desc.finalLayout,
|
||||
});
|
||||
@@ -815,6 +1132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
||||
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
|
||||
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
|
||||
// and their content is undefined anyway (EGL swap), so drop the attachment
|
||||
// and its whole tile load + store.
|
||||
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
|
||||
selectedDepthStencilAttachment = nullptr;
|
||||
}
|
||||
const Bool hasDistinctDepthAndStencilAttachments =
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
@@ -833,6 +1156,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
||||
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
|
||||
// undefined after a swap, so the first default-FBO pass of a frame can
|
||||
// skip the depth/stencil tile load outright.
|
||||
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
depthAttachmentDescription.flags = 0;
|
||||
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Int depthAttachmentId = 0;
|
||||
@@ -916,6 +1245,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = selectedDepthStencilAttachment->GetTexture(),
|
||||
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = depthAttachmentDescription.finalLayout,
|
||||
});
|
||||
@@ -961,6 +1291,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
|
||||
|
||||
// Declare only the used colour-reference span. The GL draw-buffer array
|
||||
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
|
||||
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
|
||||
// per-pixel render-backend/export path from the DECLARED count, so every
|
||||
// fragment of every pass paid the 8-target export cost (measured on
|
||||
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
|
||||
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
|
||||
// Interior GL_NONE holes keep their slots so fragment-output locations
|
||||
// still line up; a fragment output at a location past the trimmed count
|
||||
// is discarded, which is exactly GL's semantic for writing to a draw
|
||||
// buffer set to GL_NONE.
|
||||
while (!colorAttachmentRefs.empty() &&
|
||||
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
|
||||
colorAttachmentRefs.pop_back();
|
||||
}
|
||||
|
||||
// Subpass
|
||||
VkSubpassDescription subpassDesc;
|
||||
subpassDesc.flags = 0;
|
||||
@@ -974,6 +1320,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
subpassDesc.preserveAttachmentCount = 0;
|
||||
subpassDesc.pPreserveAttachments = nullptr;
|
||||
|
||||
// External subpass dependencies. Without them there is NO execution/memory
|
||||
// dependency between consecutive render passes (or a pass and a transfer)
|
||||
// touching the same attachments when the image layout does not change — the
|
||||
// layout-transition helper no-ops on identical layouts and no other barrier
|
||||
// exists. Tile-based GPUs then race tile loads against the previous pass's
|
||||
// stores (multi-pass FBO chains like MC 26.3's OIT flicker on Adreno).
|
||||
// Conservative both-ways dependencies: prior writes (attachment output,
|
||||
// depth/stencil, transfer, shader) are made visible to this pass's loads,
|
||||
// and this pass's attachment writes to subsequent sampling/transfer/loads.
|
||||
VkSubpassDependency subpassDependencies[2];
|
||||
subpassDependencies[0] = {};
|
||||
subpassDependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||
subpassDependencies[0].dstSubpass = 0;
|
||||
subpassDependencies[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
subpassDependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
subpassDependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
subpassDependencies[0].dstAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_READ_BIT;
|
||||
subpassDependencies[0].dependencyFlags = 0;
|
||||
subpassDependencies[1] = {};
|
||||
subpassDependencies[1].srcSubpass = 0;
|
||||
subpassDependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||
subpassDependencies[1].srcStageMask =
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||
subpassDependencies[1].srcAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
subpassDependencies[1].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
subpassDependencies[1].dstAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
subpassDependencies[1].dependencyFlags = 0;
|
||||
|
||||
// Render Pass
|
||||
VkRenderPassCreateInfo renderPassCreateInfo;
|
||||
renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||
@@ -983,8 +1375,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.pAttachments = attachmentDescriptions.data();
|
||||
renderPassCreateInfo.subpassCount = 1;
|
||||
renderPassCreateInfo.pSubpasses = &subpassDesc;
|
||||
renderPassCreateInfo.dependencyCount = 0;
|
||||
renderPassCreateInfo.pDependencies = nullptr;
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
@@ -1015,7 +1407,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hasDepthStencilAttachment,
|
||||
renderPassSampleCount,
|
||||
extent,
|
||||
static_cast<Int>(framebufferLayers) };
|
||||
framebufferLayers };
|
||||
MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(compatibilityHash),
|
||||
@@ -1025,9 +1417,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.x(),
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
return insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
|
||||
// is O(#renderbuffer resources) — single digits in practice — and per-frame
|
||||
// invocation keeps dead-resource reclaim latency at the aging bound instead of
|
||||
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
|
||||
// site never runs again once an app stops using renderbuffers).
|
||||
CollectRenderbufferGarbage();
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
|
||||
|
||||
// Sweep occasionally; evict entries whose last use is far past every
|
||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||
// safely (RenderPassEntry's destructor releases the handles).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the dying handles and notify once after the loop: pipelines hashed
|
||||
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
|
||||
// by draws that hit those entries), so the observer may destroy them
|
||||
// immediately - and a single batched notification costs one pipeline-cache
|
||||
// scan instead of one per evicted pass.
|
||||
Vector<VkRenderPass> destroyedRenderPasses;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
destroyedRenderPasses.push_back(it->second.renderPass);
|
||||
it = m_renderPasses.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||
// TODO: Transition all the attachments into proper layout before starting the render pass
|
||||
VkRenderPassBeginInfo renderPassBeginInfo;
|
||||
@@ -1078,6 +1515,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassBeginInfo.pClearValues = clearValues.data();
|
||||
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
// Pre-pass stream bookkeeping: this pass's attachment images are now
|
||||
// referenced by the open frame recording.
|
||||
if (s_textureManager != nullptr) {
|
||||
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
||||
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
||||
if (const auto texture = tracked.texture.lock()) {
|
||||
s_textureManager->StampTextureRecordingUse(texture.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||
if (pending.hasInlinePayload) {
|
||||
if (s_renderPassManager != nullptr) {
|
||||
@@ -1130,11 +1578,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case TrackedAttachmentTarget::SwapchainColor:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
// The pass stored into the attachment: its content is defined
|
||||
// until the image is next presented.
|
||||
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
trackedAttachment.finalLayout);
|
||||
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||
|
||||
@@ -27,7 +27,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
struct PendingClearAttachmentInfo {
|
||||
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
|
||||
Uint32 attachmentIndex = 0;
|
||||
// Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
|
||||
// draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
|
||||
// Only meaningful for color clears.
|
||||
Uint32 colorAttachmentSlot = 0;
|
||||
PendingClearKey key{};
|
||||
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
|
||||
Bool hasInlinePayload = false;
|
||||
@@ -37,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct TrackedAttachmentLayoutInfo {
|
||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||
// May dangle once the texture dies - compare only, never dereference.
|
||||
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
Uint32 textureMipLevel = 0;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
@@ -68,7 +78,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool hasDepthStencilAttachment = false;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
IntVec2 extent = {0, 0};
|
||||
Uint32 subpass = 0;
|
||||
// VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments).
|
||||
Uint32 layers = 1;
|
||||
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
|
||||
Uint64 lastUsedFrame = 0;
|
||||
|
||||
RenderPassEntry() = default;
|
||||
RenderPassEntry(const RenderPassEntry&) = delete;
|
||||
@@ -84,7 +97,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
std::swap(sampleCount, that.sampleCount);
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(subpass, that.subpass);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
}
|
||||
RenderPassEntry(
|
||||
Uint64 hash,
|
||||
@@ -97,7 +111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 colorAttachmentCount,
|
||||
Bool hasDepthStencilAttachment,
|
||||
VkSampleCountFlagBits sampleCount,
|
||||
IntVec2 extent, int subpass):
|
||||
IntVec2 extent, Uint32 layers):
|
||||
hash(hash),
|
||||
renderPass(renderpass),
|
||||
framebuffer(framebuffer),
|
||||
@@ -109,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hasDepthStencilAttachment(hasDepthStencilAttachment),
|
||||
sampleCount(sampleCount),
|
||||
extent(extent),
|
||||
subpass(subpass)
|
||||
layers(layers)
|
||||
{}
|
||||
|
||||
~RenderPassEntry() {
|
||||
@@ -148,24 +162,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkRenderPassManager {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
||||
// everything keyed on them before any new pass can be created (the sweep and
|
||||
// the notification run back-to-back with no creation in between; observers
|
||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
||||
// scan, not one per dying pass. The wholesale paths
|
||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
||||
// every pipeline outright.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
||||
};
|
||||
|
||||
VkRenderPassManager(VkDevice device,
|
||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||
~VkRenderPassManager();
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool includePendingClear = true);
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
|
||||
Bool includePendingClear = true,
|
||||
Bool includeDefaultFboDepthStencil = true);
|
||||
// drawUsesDepthStencil: whether the operation about to run inside the pass
|
||||
// reads or writes the depth/stencil buffer (depth test or stencil test
|
||||
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
|
||||
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
|
||||
// default-FBO pass whose draws provably never touch depth/stencil is
|
||||
// created WITHOUT the depth attachment - on a tiler that skips the whole
|
||||
// depth tile load AND store. The flavor only escalates: once a pass with
|
||||
// depth is active, later depth-less draws keep using it, and a depth-using
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
|
||||
// Frame boundary hook: ages the render-pass cache and evicts long-unused
|
||||
// entries (their command buffers retired many frames ago).
|
||||
void OnPresent();
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
|
||||
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
|
||||
static ActiveRenderPassInfo* GetActiveRenderPass();
|
||||
@@ -178,12 +229,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager& m_textureManager;
|
||||
SwapchainObject& m_swapchainObject;
|
||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
|
||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||
// image recreation.
|
||||
Uint64 m_renderbufferImageEpoch = 1;
|
||||
|
||||
public:
|
||||
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
|
||||
// snapshots include it so an attachment respecify forces a re-resolve.
|
||||
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
|
||||
|
||||
private:
|
||||
|
||||
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
|
||||
// framebuffer state is provably unchanged since the last resolution, the active render pass
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
@@ -196,12 +257,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
// Whether the memoized entry carries a depth/stencil attachment; a
|
||||
// default-FBO resolution whose effective depth request differs must
|
||||
// miss the memo (the depth-less/depth-full flavors hash differently).
|
||||
Bool m_rpFastHadDepthStencil = false;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
||||
// expired. Dead resources age past every in-flight frame before Destroy
|
||||
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
||||
// for frames-in-flight frames after the GL object dies.
|
||||
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
||||
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
// UNORM reinterpretation of an sRGB image, used as the attachment view while
|
||||
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -209,25 +284,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
Int samples = 0;
|
||||
// m_frameCounter value at which the weak reference was first seen expired.
|
||||
Uint64 deadSinceFrame = kNeverObservedDead;
|
||||
|
||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||
};
|
||||
|
||||
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
|
||||
// attachments the same way texture attachments go through the texture manager.
|
||||
RenderbufferResource* GetOrCreateRenderbufferResource(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
|
||||
ClearAttachmentPayload& outPayload) const;
|
||||
|
||||
private:
|
||||
struct PendingRenderbufferClear {
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
||||
// until enough frame boundaries have passed that no in-flight command buffer
|
||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
||||
struct DeferredRenderbufferRelease {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
|
||||
|
||||
RenderbufferResource* GetOrCreateRenderbufferResource(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
|
||||
ClearAttachmentPayload& outPayload) const;
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
void CollectRenderbufferGarbage();
|
||||
// Frame-boundary margin after which a resource last referenced by a retired
|
||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
||||
Uint64 RetireAgeFrames() const;
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
|
||||
@@ -51,6 +51,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||
}
|
||||
|
||||
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
|
||||
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
|
||||
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
|
||||
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
|
||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
|
||||
@@ -58,11 +70,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_device = initInfo.device;
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
}
|
||||
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
Bool forceNearestFiltering) const {
|
||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||
if (forceNearestFiltering) return 1.0f;
|
||||
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
|
||||
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
|
||||
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
|
||||
sampler.GetMagFilter() != SamplerFilterMode::Linear) {
|
||||
return 1.0f;
|
||||
}
|
||||
return std::clamp(sampler.GetMaxAnisotropy(), 1.0f, m_maxSamplerAnisotropy);
|
||||
}
|
||||
|
||||
void VkSamplerManager::Shutdown() {
|
||||
for (auto& [_, sampler] : m_samplers) {
|
||||
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
|
||||
@@ -74,13 +101,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; destroy samplers whose last use is far past every
|
||||
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
|
||||
// double-free the handle; an evicted key that recurs simply re-creates
|
||||
// its sampler on the next miss.
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
|
||||
auto& entry = it->second;
|
||||
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
const auto magFilter = sampler.GetMagFilter();
|
||||
@@ -93,12 +151,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
const auto lodBias = sampler.GetLodBias();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
|
||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = ResolveCompareFunc(sampler, texture);
|
||||
@@ -109,27 +172,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture);
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
||||
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
|
||||
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
|
||||
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
|
||||
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return it->second.handle;
|
||||
}
|
||||
|
||||
VkSamplerCreateInfo samplerInfo{};
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
|
||||
? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||
samplerInfo.mipLodBias = sampler.GetLodBias();
|
||||
samplerInfo.anisotropyEnable = VK_FALSE;
|
||||
samplerInfo.maxAnisotropy = 1.0f;
|
||||
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
|
||||
// different samplers or silently create duplicates.
|
||||
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
||||
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
@@ -141,6 +221,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.handle = vkSampler;
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
|
||||
@@ -24,23 +24,47 @@ public:
|
||||
struct InitInfo {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig* config = nullptr;
|
||||
// The samplerAnisotropy device feature was requested and granted at vkCreateDevice.
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
// viewLevelCount is the mip-level count of the image view this sampler will be paired
|
||||
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false,
|
||||
Uint32 viewLevelCount = 0);
|
||||
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
||||
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
||||
// anisotropy), so an app animating those would otherwise mint an unbounded
|
||||
// stream of never-destroyed VkSamplers and eventually exhaust the device's
|
||||
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
|
||||
// boundaries cannot be referenced by any in-flight command buffer (frames in
|
||||
// flight are single digits), and every descriptor set the GPU consumes is
|
||||
// written that same frame with live handles (the per-binding resolve memo and
|
||||
// descriptor-set reuse are both frame-reset), so destruction here needs no
|
||||
// fence wait. Self-gated: one counter bump and compare except on sweep
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
Uint externalIndex = 0;
|
||||
Uint16 version = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
@@ -49,10 +73,20 @@ private:
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
|
||||
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
Bool forceNearestFiltering) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,18 +13,24 @@
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class ITextureObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
@@ -50,6 +56,9 @@ public:
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
||||
Uint32 frameCount = 0;
|
||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||
Bool imageFormatListSupported = false;
|
||||
};
|
||||
|
||||
struct TextureResource {
|
||||
@@ -58,12 +67,16 @@ public:
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
// May differ from the image format: sRGB images attach through their UNORM
|
||||
// twin while GL_FRAMEBUFFER_SRGB is disabled.
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const AttachmentViewKey& other) const {
|
||||
return mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType;
|
||||
viewType == other.viewType &&
|
||||
viewFormat == other.viewFormat;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,6 +87,63 @@ public:
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct StorageImageViewKey {
|
||||
Uint32 mipLevel = 0;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const StorageImageViewKey& other) const {
|
||||
return mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format;
|
||||
}
|
||||
};
|
||||
|
||||
struct SampledImageViewKey {
|
||||
Uint32 baseMipLevel = 0;
|
||||
Uint32 levelCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const SampledImageViewKey& other) const {
|
||||
return baseMipLevel == other.baseMipLevel &&
|
||||
levelCount == other.levelCount &&
|
||||
viewType == other.viewType &&
|
||||
format == other.format;
|
||||
}
|
||||
};
|
||||
|
||||
struct SampledImageViewKeyHash {
|
||||
SizeT operator()(const SampledImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct StorageImageViewKeyHash {
|
||||
SizeT operator()(const StorageImageViewKey& key) const {
|
||||
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
|
||||
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -85,6 +155,8 @@ public:
|
||||
Vector<VkImageView> perMipViews;
|
||||
Vector<VkImageView> perMipSampledViews;
|
||||
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
|
||||
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
|
||||
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkExtent2D extent = {0, 0};
|
||||
Uint32 depth = 1;
|
||||
@@ -96,7 +168,26 @@ public:
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
// Usage the live image was created with. STORAGE is only requested for textures that
|
||||
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
||||
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
||||
// and recreates the image, so the resolved usage has to be part of the compatibility
|
||||
// check that decides whether the existing image can be kept.
|
||||
VkImageUsageFlags usageFlags = 0;
|
||||
// True once this image was (re)resolved while the texture was already marked as an
|
||||
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
||||
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||
Bool storageUsageResolved = false;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
|
||||
// command referencing this image that was recorded into the CURRENT frame
|
||||
// command buffer. An image untouched by the open recording may have its
|
||||
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -115,6 +206,8 @@ public:
|
||||
std::swap(this->perMipViews, that.perMipViews);
|
||||
std::swap(this->perMipSampledViews, that.perMipSampledViews);
|
||||
std::swap(this->attachmentViews, that.attachmentViews);
|
||||
std::swap(this->alternateSampledViews, that.alternateSampledViews);
|
||||
std::swap(this->storageImageViews, that.storageImageViews);
|
||||
std::swap(this->layout, that.layout);
|
||||
std::swap(this->extent, that.extent);
|
||||
std::swap(this->depth, that.depth);
|
||||
@@ -126,7 +219,11 @@ public:
|
||||
std::swap(this->aspect, that.aspect);
|
||||
std::swap(this->viewType, that.viewType);
|
||||
std::swap(this->sampleCount, that.sampleCount);
|
||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||
std::swap(this->usageFlags, that.usageFlags);
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
}
|
||||
@@ -153,6 +250,16 @@ public:
|
||||
vkDestroyImageView(s_device, attachmentView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, sampledView] : alternateSampledViews) {
|
||||
if (sampledView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, sampledView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, storageImageView] : storageImageViews) {
|
||||
if (storageImageView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, storageImageView, nullptr);
|
||||
}
|
||||
}
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(s_allocator, image, allocation);
|
||||
}
|
||||
@@ -161,6 +268,8 @@ public:
|
||||
perMipViews.clear();
|
||||
perMipSampledViews.clear();
|
||||
attachmentViews.clear();
|
||||
alternateSampledViews.clear();
|
||||
storageImageViews.clear();
|
||||
image = VK_NULL_HANDLE;
|
||||
allocation = nullptr;
|
||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -174,6 +283,9 @@ public:
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
usageFlags = 0;
|
||||
storageUsageResolved = false;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
@@ -190,6 +302,10 @@ public:
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
@@ -198,6 +314,9 @@ public:
|
||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||
VkImageViewType viewType);
|
||||
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
|
||||
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
VkFormat format, Bool layered, Int32 layer);
|
||||
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
|
||||
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject* texture,
|
||||
@@ -206,7 +325,41 @@ public:
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
// recording; a resource whose stamp does not match was not referenced by
|
||||
// any command in the open recording, so its out-of-pass work may safely
|
||||
// execute ahead of the whole recording (in the pre command buffer).
|
||||
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
|
||||
void StampResourceRecordingUse(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
// Map-lookup variant for callers that only hold the GL texture object.
|
||||
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
||||
// would cost far more than the compression it wins back.
|
||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||
|
||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
@@ -243,6 +396,9 @@ public:
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
TextureResource &outResource);
|
||||
@@ -255,7 +411,8 @@ private:
|
||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 baseArrayLayer,
|
||||
Uint32 layerCount,
|
||||
const VkComponentMapping* components = nullptr) const;
|
||||
const VkComponentMapping* components = nullptr,
|
||||
VkImageUsageFlags viewUsage = 0) const;
|
||||
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
||||
TextureUploadTarget uploadTarget,
|
||||
TextureResource &outResource);
|
||||
@@ -272,18 +429,28 @@ private:
|
||||
void DeferViewRelease(VkImageView view);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyDeferredReleases();
|
||||
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
||||
// upload whose fence has signaled (submission order = completion order on
|
||||
// the single queue, so the scan stops at the first still-pending entry).
|
||||
// waitAll blocks on every entry - Shutdown's drain.
|
||||
void ReclaimCompletedUploads(Bool waitAll = false);
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
SizeT PruneDeadTextures();
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||
Bool m_imageFormatListSupported = false;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
||||
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
||||
Uint32 m_gcFrameCounter = 0;
|
||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||
// textures already fully synced in the current draw (small N -> flat scan).
|
||||
Bool m_drawSyncScopeActive = false;
|
||||
@@ -296,9 +463,45 @@ private:
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
||||
// are resolved on every draw, so cache their resource pointers and skip the
|
||||
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
||||
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
||||
// every memo entry must match. SyncTexture still runs on memo hits, so
|
||||
// content/param freshness is unaffected. A dead-then-reused texture address
|
||||
// cannot false-hit: the new object carries a new lifetime id.
|
||||
struct SyncedTextureMemoEntry {
|
||||
const MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
Uint64 eraseEpoch = 0;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
||||
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
||||
Uint32 m_syncedTextureMemoNext = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||
// behind the queue serialized the CPU against the previous frame's GPU
|
||||
// work every time an animated atlas re-uploaded). Their transient objects
|
||||
// are parked here and reclaimed once the upload fence signals.
|
||||
struct PendingUploadReclaim {
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||
VmaAllocation stagingAllocation = nullptr;
|
||||
};
|
||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 instanceCount = 1;
|
||||
Uint32 firstVertex = 0;
|
||||
Uint32 firstInstance = 0;
|
||||
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
|
||||
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
|
||||
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
|
||||
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
|
||||
Int32 baseVertex = 0;
|
||||
Bool indexRangeIsExactView = false;
|
||||
};
|
||||
|
||||
struct DrawIndexedCmdParam {
|
||||
@@ -70,6 +76,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum indexType = GL_UNSIGNED_SHORT;
|
||||
SizeT indexByteOffset = 0;
|
||||
SizeT indexByteSize = 0;
|
||||
// Interpret indexByteOffset as a raw client pointer even when an element
|
||||
// array buffer is bound (backend-synthesized index lists, e.g. the
|
||||
// GL_LINE_LOOP -> LINE_STRIP rewrite).
|
||||
Bool forceClientMemory = false;
|
||||
};
|
||||
|
||||
struct DrawIndexedCmd {
|
||||
@@ -108,7 +118,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
};
|
||||
|
||||
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
|
||||
class VulkanRenderer : public IBufferCopyCommandProvider,
|
||||
public FrameContext::IRecordingObserver,
|
||||
public VkRenderPassManager::IEvictionObserver,
|
||||
public ProgramFactory::IEvictionObserver {
|
||||
public:
|
||||
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
||||
~VulkanRenderer();
|
||||
@@ -125,12 +138,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recording, before any render pass.
|
||||
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
||||
|
||||
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
|
||||
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
|
||||
// dying handle (they share its >1024-boundary idleness, so immediate
|
||||
// destruction is safe) and drop the last-pipeline memo if any went.
|
||||
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
|
||||
|
||||
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
||||
// destroyed; evict its compute pipeline and graphics pipelines (same
|
||||
// idleness guarantee - they are only bound through draws/dispatches that
|
||||
// stamp the program entry) and purge the descriptor-set cache entries
|
||||
// keyed by its now-recyclable VkDescriptorSetLayout handle.
|
||||
void OnProgramEvicted(ProgramFactory::HashType programHash,
|
||||
VkDescriptorSetLayout descriptorSetLayout) override;
|
||||
|
||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
|
||||
// resolved draw configuration; the next draw whose cheap version/identity
|
||||
// checks all match skips the resolution half (LOD probe, sampled-set
|
||||
// walk, render-pass and pipeline resolution) and jumps straight to the
|
||||
// per-draw tail. Returns false (leaving no side effects that the full
|
||||
// path cannot redo idempotently) whenever anything might have changed.
|
||||
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
|
||||
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||
const RenderPassEntry& compatibleRenderPassEntry);
|
||||
|
||||
enum class ScissoredClearPrep {
|
||||
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
|
||||
NoOp, // nothing to clear (degenerate target or empty scissor rect)
|
||||
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
|
||||
};
|
||||
ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
VkClearRect& outClearRect);
|
||||
|
||||
void Clear(GLbitfield mask);
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
@@ -157,6 +200,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
|
||||
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
|
||||
// CPU repacking into the requested client layout).
|
||||
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels);
|
||||
// Same-extent depth blit between images of different depth formats: host
|
||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
|
||||
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
|
||||
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
Uint8* destinationPixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
||||
@@ -219,6 +286,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// frontend. Timestamp support (queue timestampValidBits > 0 and a
|
||||
// non-zero timestampPeriod) is cached at device creation.
|
||||
Bool IsTimerQuerySupported() const;
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -234,8 +304,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkTimerQueryManager::TimestampRecord& end) const;
|
||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||
|
||||
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
|
||||
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
|
||||
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
|
||||
// unsupported) when the device lacks it.
|
||||
Bool StartOcclusionQueryCapture();
|
||||
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
|
||||
// Flushes pending commands, waits, sums the slots, and recycles them.
|
||||
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
void RecreateSwapchain();
|
||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
||||
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
||||
Bool SwapchainIsOutOfDate();
|
||||
// Returns false when the surface is zero-area (minimized/hidden window):
|
||||
// no new swapchain is installed and presentation must stay suspended.
|
||||
Bool RecreateSwapchain();
|
||||
|
||||
private:
|
||||
struct BlitUniformData {
|
||||
@@ -275,6 +360,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload);
|
||||
void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload,
|
||||
const VkClearRect& clearRect);
|
||||
|
||||
// ---- Submission fence tracking (GL sync objects) ----
|
||||
// One record per vkQueueSubmit still in flight, in ascending submit
|
||||
@@ -317,24 +406,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFence AcquirePooledSubmitFence();
|
||||
void DestroySubmitFencePool();
|
||||
Bool HasPendingRecordedWork() const;
|
||||
// Frame-boundary housekeeping for paths that never reach Present's
|
||||
// tail (present-less readback loops, suspended presentation, blocking
|
||||
// sync waits): runs the same per-frame drains Present performs, but
|
||||
// only when every queue submission has been observed complete AND no
|
||||
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
|
||||
// is provably already zero. Never blocks (non-blocking fence poll
|
||||
// only), so the presenting path's frames-in-flight pipelining is
|
||||
// untouched. Returns true when the drain ran.
|
||||
Bool TryDrainFrameTransients();
|
||||
|
||||
Vector<SubmitRecord> m_inFlightSubmits;
|
||||
Vector<VkFence> m_freeSubmitFences;
|
||||
Uint64 m_submitCounter = 0;
|
||||
Uint64 m_completedSubmitCounter = 0;
|
||||
// Drains since the last Present, gating the drain's frame-boundary-equivalent
|
||||
// work (arena rewind + cache aging): a presenting app's mid-frame
|
||||
// readbacks/waits must neither churn the transient caches nor accelerate the
|
||||
// aging clocks, while present-less loops still cross a boundary every few
|
||||
// iterations. Reset in Present.
|
||||
Uint32 m_drainsSinceLastPresent = 0;
|
||||
|
||||
NativeWindowType m_window = 0;
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
VulkanRendererConfig m_config;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
// swapchain is unusable/out of date, so Present drops frames instead of
|
||||
// submitting on a signaled fence / presenting never-acquired images.
|
||||
Bool m_presentSuspended = false;
|
||||
|
||||
// Vulkan objects
|
||||
Bool m_validationLayersEnabled = false;
|
||||
Vector<VkExtensionProperties> m_extensions;
|
||||
VkInstance m_instance = VK_NULL_HANDLE;
|
||||
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
|
||||
// Fallback reporting channel for drivers that ship the validation layers but
|
||||
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
|
||||
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
|
||||
PhysicalDevice m_physicalDevice;
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
@@ -347,8 +467,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool m_indexTypeUint8ExtensionEnabled = false;
|
||||
Bool m_logicOpFeatureEnabled = false;
|
||||
Bool m_multiDrawIndirectFeatureEnabled = false;
|
||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||
// drive a runtime fallback when the device lacks them.
|
||||
@@ -370,6 +492,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 stride);
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
||||
// draws within one glBeginTransformFeedback append GL-style.
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
// Non-zero while inside a GL Begin/End with at least one captured draw
|
||||
// recorded; selects counter-buffer resume on the next captured draw.
|
||||
Bool m_xfbCountersValid = false;
|
||||
Uint64 m_xfbLastSeenGeneration = 0;
|
||||
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
||||
// when GL transform feedback is active; binds capture buffers on demand.
|
||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
||||
// query is active. Returns whether a slot was begun (End must mirror it).
|
||||
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
|
||||
Bool m_occlusionQueryPreciseEnabled = false;
|
||||
Bool m_hostQueryResetEnabled = false;
|
||||
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
|
||||
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kOcclusionQuerySlots = 8192;
|
||||
Uint32 m_occlusionSlotCursor = 0;
|
||||
Bool m_occlusionCaptureActive = false;
|
||||
Vector<Uint32> m_occlusionActiveSlots;
|
||||
// Transform feedback primitive queries: one pool slot per captured draw yields
|
||||
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
|
||||
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
|
||||
// unlike the CPU fallback accounting.
|
||||
Bool m_xfbQueriesSupported = false;
|
||||
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
|
||||
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
|
||||
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kXfbQuerySlots = 8192;
|
||||
Uint32 m_xfbQuerySlotCursor = 0;
|
||||
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
|
||||
public:
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
|
||||
VkBufferManager m_bufferManager;
|
||||
@@ -382,14 +557,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
||||
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
||||
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
||||
Bool m_lastPipelineValid = false;
|
||||
GLenum m_lastPipelineMode = 0;
|
||||
Uint64 m_lastPipelineProgramHash = 0;
|
||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
||||
Uint m_lastPipelineRenderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||
// single-entry memo thrashed on draw sequences that alternate a few
|
||||
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||
// payload-hash lookup per draw; eight entries cover such working sets
|
||||
// while keeping the hit path a trivial linear scan.
|
||||
struct PipelineMemoEntry {
|
||||
GLenum mode = 0;
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint renderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 0;
|
||||
Uint32 m_pipelineMemoNext = 0;
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed.
|
||||
void InvalidatePipelineMemo() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
}
|
||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||
UniquePtr<ProgramFactory> m_programFactory;
|
||||
UniquePtr<UniformManager> m_uniformManager;
|
||||
@@ -418,16 +609,125 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
// sampled-set memo, plus the sampled textures' params-version sum so a
|
||||
// level-range or filter change re-probes. On a hit the resolved
|
||||
// transform flags are reused, which also collapses the two
|
||||
// GetOrCreateProgram lookups into one.
|
||||
Bool m_lastLodDecisionValid = false;
|
||||
Uint64 m_lastLodProgramLifetimeId = 0;
|
||||
Uint32 m_lastLodProgramVersion = 0;
|
||||
Uint64 m_lastLodBindGeneration = 0;
|
||||
Uint64 m_lastLodParamsSum = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
// protected by the command-buffer-boundary reset plus the mid-frame
|
||||
// pipeline-destruction resets, and monotonic epochs guard everything
|
||||
// that can be destroyed or recreated between draws.
|
||||
struct SetupDrawSnapshot {
|
||||
Bool valid = false;
|
||||
Uint8 aspects = 0;
|
||||
GLenum mode = 0;
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 programVersion = 0;
|
||||
const void* vao = nullptr;
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
Uint16 fboVersion = 0;
|
||||
Bool drawFboIsDefault = false;
|
||||
Uint renderStateVersion = 0;
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
Uint64 textureImageEpoch = 0;
|
||||
Uint64 renderbufferImageEpoch = 0;
|
||||
Uint64 sampledContentSum = 0;
|
||||
Uint64 sampledParamsSum = 0;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
||||
|
||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||
// draw call and must not allocate.
|
||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||
// first sampled-texture loop: the resolved backend resources, so the
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||
Vector<Float> m_vertexConversionScratch;
|
||||
Vector<Uint8> m_vertexRepackScratch;
|
||||
|
||||
struct ConvertedVertexStreamKey {
|
||||
const MG_State::GLState::BufferObject* buffer = nullptr;
|
||||
Uint64 changeSerial = 0;
|
||||
SizeT baseOffset = 0;
|
||||
Uint32 sourceStride = 0;
|
||||
DataType type = DataType::Float32;
|
||||
Int size = 0;
|
||||
Bool normalized = false;
|
||||
Bool isInteger = false;
|
||||
VertexInputStateFactory::VertexStreamConversion conversion =
|
||||
VertexInputStateFactory::VertexStreamConversion::None;
|
||||
|
||||
Bool operator==(const ConvertedVertexStreamKey& other) const {
|
||||
return buffer == other.buffer && changeSerial == other.changeSerial &&
|
||||
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
|
||||
type == other.type && size == other.size && normalized == other.normalized &&
|
||||
isInteger == other.isInteger && conversion == other.conversion;
|
||||
}
|
||||
};
|
||||
|
||||
struct ConvertedVertexStreamKeyHash {
|
||||
SizeT operator()(const ConvertedVertexStreamKey& key) const {
|
||||
SizeT hash = std::hash<const void*>{}(key.buffer);
|
||||
auto combine = [&hash](SizeT value) {
|
||||
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
|
||||
};
|
||||
combine(std::hash<Uint64>{}(key.changeSerial));
|
||||
combine(std::hash<SizeT>{}(key.baseOffset));
|
||||
combine(std::hash<Uint32>{}(key.sourceStride));
|
||||
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
|
||||
combine(std::hash<Int>{}(key.size));
|
||||
combine(std::hash<Bool>{}(key.normalized));
|
||||
combine(std::hash<Bool>{}(key.isInteger));
|
||||
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct ConvertedVertexStream {
|
||||
BufferSlice slice;
|
||||
// Number of source elements the cached slice covers. A draw needing a prefix of
|
||||
// this range reuses the slice (converted streams are tightly packed); a draw
|
||||
// needing more reconverts and replaces the entry, so per (buffer, layout) a
|
||||
// frame converts at most the largest range any draw asked for.
|
||||
SizeT elementCount = 0;
|
||||
// Pins the source buffer for the frame so its heap address cannot be reused by
|
||||
// a new BufferObject while this pointer-keyed entry is alive.
|
||||
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
|
||||
};
|
||||
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
||||
m_convertedVertexStreams;
|
||||
|
||||
void CreateInstance();
|
||||
VkResult SetupDebugMessenger();
|
||||
VkResult DestroyDebugMessenger();
|
||||
VkResult SetupDebugReportCallback();
|
||||
void DestroyDebugReportCallback();
|
||||
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
||||
void CreateSurface();
|
||||
void PickPhysicalDevice();
|
||||
@@ -446,10 +746,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
// flush the pending recording (see the body), which retires the current command buffer.
|
||||
Bool PrepareStorageImageTextures(
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const DrawCmdParam& drawParams);
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView);
|
||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
@@ -467,6 +774,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum filter);
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
Bool MaterializePendingClearForRenderbuffer(
|
||||
VkCommandBuffer commandBuffer,
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
@@ -501,6 +811,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const PhysicalDevice& compareWithDevice,
|
||||
PhysicalDevice& outBetterDevice);
|
||||
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
||||
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
|
||||
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
|
||||
Bool m_imageFormatListExtensionEnabled = false;
|
||||
|
||||
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
||||
static Bool CheckValidationLayerSupport();
|
||||
|
||||
|
||||
@@ -52,19 +52,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
|
||||
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
|
||||
// attachment view (and render pass format) therefore drops to the UNORM twin
|
||||
// whenever the capability is off. Sampled views keep the sRGB format (decode on
|
||||
// sample is unconditional in GL).
|
||||
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
|
||||
if (framebufferSrgbEnabled) return format;
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
default:
|
||||
return format;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||
// call: appending its format to the base format while its arguments precede the base
|
||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
if (_vk_verify_result != VK_SUCCESS) { \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define XXHASH_VERIFY(expr, ...) \
|
||||
do { \
|
||||
XXH_errorcode _xxh_verify_result = (expr); \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
|
||||
if (_xxh_verify_result != XXH_OK) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
|
||||
__LINE__); \
|
||||
} while (0)
|
||||
|
||||
@@ -16,4 +16,5 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_test(NAME BufferBench COMMAND BufferBench --benchmark_counters_tabular=true)
|
||||
add_test(NAME BufferBench COMMAND BufferBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(BufferBench PROPERTIES LABELS benchmark)
|
||||
@@ -38,6 +38,7 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
@@ -16,4 +16,5 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
|
||||
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(ProgramBench PROPERTIES LABELS benchmark)
|
||||
@@ -24,6 +24,7 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
GLint Samples = 0;
|
||||
GLint Profile = kCGLOGLPVersion_3_2_Core;
|
||||
GLint RendererId = 0x4d474c;
|
||||
GLint DisplayMask = 0;
|
||||
};
|
||||
|
||||
struct ContextObject {
|
||||
@@ -134,6 +135,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
case kCGLPFARendererID:
|
||||
pixelFormat.RendererId = value;
|
||||
break;
|
||||
case kCGLPFADisplayMask:
|
||||
pixelFormat.DisplayMask = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -343,6 +347,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
case kCGLPFARendererID:
|
||||
*value = pixelFormat->RendererId;
|
||||
return kCGLNoError;
|
||||
case kCGLPFADisplayMask:
|
||||
*value = pixelFormat->DisplayMask;
|
||||
return kCGLNoError;
|
||||
case kCGLPFAOpenGLProfile:
|
||||
*value = pixelFormat->Profile;
|
||||
return kCGLNoError;
|
||||
@@ -481,6 +488,32 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
return it == currentContexts.end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
if (!object) {
|
||||
return kCGLBadContext;
|
||||
}
|
||||
if (screen != 0) {
|
||||
return kCGLBadValue;
|
||||
}
|
||||
object->VirtualScreen = screen;
|
||||
return kCGLNoError;
|
||||
}
|
||||
|
||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
if (!object) {
|
||||
return kCGLBadContext;
|
||||
}
|
||||
if (!screen) {
|
||||
return kCGLBadAddress;
|
||||
}
|
||||
*screen = object->VirtualScreen;
|
||||
return kCGLNoError;
|
||||
}
|
||||
|
||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(ctx);
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
||||
|
||||
CGLError SetCurrentContext(CGLContextObj ctx);
|
||||
CGLContextObj GetCurrentContext();
|
||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
|
||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
|
||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
|
||||
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
|
||||
CGLError UpdateContext(CGLContextObj ctx);
|
||||
|
||||
@@ -71,6 +71,14 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
|
||||
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
||||
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
||||
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
|
||||
}
|
||||
|
||||
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include "MG_Impl/CGLImpl/CGLImpl.h"
|
||||
#include "MG_Impl/GetProcAddress.h"
|
||||
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <CoreVideo/CVDisplayLink.h>
|
||||
#include <cstdint>
|
||||
#include <dlfcn.h>
|
||||
|
||||
namespace {
|
||||
@@ -47,10 +51,52 @@ namespace {
|
||||
return dlsym(handle, symbol);
|
||||
}
|
||||
|
||||
CGDirectDisplayID DisplayForMask(GLint displayMask) {
|
||||
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
|
||||
CGDirectDisplayID displays[MaxDisplays] = {};
|
||||
std::uint32_t displayCount = 0;
|
||||
if (displayMask != 0 &&
|
||||
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
|
||||
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
|
||||
for (std::uint32_t i = 0; i < displayCount; ++i) {
|
||||
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
|
||||
return displays[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return CGMainDisplayID();
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
|
||||
CVDisplayLinkRef displayLink,
|
||||
CGLContextObj context,
|
||||
CGLPixelFormatObj pixelFormat) {
|
||||
GLint virtualScreen = 0;
|
||||
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
|
||||
GLint displayMask = 0;
|
||||
if (!displayLink ||
|
||||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
|
||||
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
|
||||
return kCVReturnInvalidArgument;
|
||||
}
|
||||
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
|
||||
}
|
||||
|
||||
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
|
||||
static const auto original = reinterpret_cast<OriginalFunction>(
|
||||
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
|
||||
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
|
||||
}
|
||||
|
||||
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
|
||||
__attribute__((section("__DATA,__interpose"))) = {
|
||||
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
|
||||
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
|
||||
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
|
||||
};
|
||||
#pragma clang diagnostic pop
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Public CGL entry points.
|
||||
_CGL*
|
||||
|
||||
# Public EGL entry points.
|
||||
_egl*
|
||||
|
||||
# Public OpenGL and GLX entry points. OpenGL function names always use an
|
||||
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
|
||||
# deliberately prevents glslang_* from matching this pattern.
|
||||
_gl[A-Z0-9]*
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "EGLImpl.h"
|
||||
#include "../GetProcAddress.h"
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <mutex>
|
||||
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return MG_State::pEGLContext.get();
|
||||
}
|
||||
|
||||
// Entry points that can legitimately be an application's FIRST EGL
|
||||
// call (display/proc-address/string queries) lazily bring MobileGL
|
||||
// up here, so the library needs no static constructor and can
|
||||
// re-initialize after the last eglTerminate tore everything down.
|
||||
// Teardown-ish entry points keep using GetState() and fail benignly
|
||||
// when MobileGL is not initialized.
|
||||
EGLStateContext* GetStateEnsureInitialized() {
|
||||
MobileGL::EnsureInitialized();
|
||||
return GetState();
|
||||
}
|
||||
|
||||
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
|
||||
auto* backendObject = MG_Backend::pActiveBackendObject.get();
|
||||
if (!backendObject && state) {
|
||||
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return MG_Backend::WindowBackend::Android;
|
||||
#elif defined(__APPLE__)
|
||||
return MG_Backend::WindowBackend::MetalLayer;
|
||||
#elif defined(_WIN32)
|
||||
return MG_Backend::WindowBackend::Win32;
|
||||
#elif defined(__linux__)
|
||||
return MG_Backend::WindowBackend::X11;
|
||||
#else
|
||||
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
auto* state = GetState();
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
EGLDisplay GetDisplay(NativeDisplayType display) {
|
||||
auto* state = GetState();
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_NO_DISPLAY;
|
||||
}
|
||||
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
backendObject->ReleaseEGLResources();
|
||||
}
|
||||
// The last initialized display is gone and nothing is current on any
|
||||
// thread: tear the whole library down deterministically inside the
|
||||
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
|
||||
// re-initializes lazily via GetStateEnsureInitialized(); process exit
|
||||
// then has nothing left to destroy.
|
||||
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
|
||||
MobileGL::Destroy();
|
||||
}
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
EGLBoolean BindAPI(EGLenum api) {
|
||||
auto* state = GetState();
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
|
||||
char const* QueryString(EGLDisplay display, EGLint name) {
|
||||
auto* state = GetState();
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
|
||||
(void)attrib_list;
|
||||
|
||||
auto* state = GetState();
|
||||
auto* state = GetStateEnsureInitialized();
|
||||
if (!state) {
|
||||
return EGL_NO_DISPLAY;
|
||||
}
|
||||
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!name) {
|
||||
return nullptr;
|
||||
}
|
||||
MobileGL::EnsureInitialized();
|
||||
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
|
||||
@@ -441,7 +441,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = buffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
if (!BufferImpl::ValidateBufferName(bufferName, true)) continue;
|
||||
// GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently
|
||||
// ignored here, so probe with the non-recording query - the shared validator would
|
||||
// record INVALID_OPERATION, which is only correct on the bind path.
|
||||
if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue;
|
||||
MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
@@ -1348,6 +1351,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||
@@ -1381,6 +1392,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
@@ -60,6 +60,11 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||
}
|
||||
if (target == BufferTarget::TransformFeedback) {
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||
pointCount = std::min<SizeT>(pointCount, 4);
|
||||
}
|
||||
|
||||
if (index < pointCount) {
|
||||
return true;
|
||||
|
||||
@@ -48,6 +48,73 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Primitives a draw of `count` vertices in `mode` assembles (0 for
|
||||
// incomplete primitives). Used for the CPU-side transform feedback
|
||||
// primitive accounting.
|
||||
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
|
||||
if (count <= 0) return 0;
|
||||
switch (mode) {
|
||||
case GL_POINTS: return static_cast<Uint64>(count);
|
||||
case GL_LINES: return static_cast<Uint64>(count / 2);
|
||||
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
|
||||
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
|
||||
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate the transform feedback primitive counter for a captured draw.
|
||||
// Draws without a geometry stage write exactly the primitives they assemble,
|
||||
// clamped by the capture buffers' remaining capacity (a full buffer stops
|
||||
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
|
||||
// Geometry amplification is not modelled here.
|
||||
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
|
||||
Uint64 primitives = CountPrimitivesForDraw(mode, count);
|
||||
if (primitives == 0) return;
|
||||
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
|
||||
|
||||
Uint64 verticesPerPrimitive = 1;
|
||||
switch (mode) {
|
||||
case GL_LINES:
|
||||
case GL_LINE_STRIP:
|
||||
case GL_LINE_LOOP:
|
||||
verticesPerPrimitive = 2;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN:
|
||||
verticesPerPrimitive = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
if (program != nullptr) {
|
||||
// Capacity in captured vertices = the tightest bound buffer.
|
||||
Uint64 capacityVertices = ~0ull;
|
||||
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
if (stride == 0) continue;
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
const Range1D range = point.GetRange();
|
||||
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
|
||||
}
|
||||
if (capacityVertices != ~0ull) {
|
||||
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
|
||||
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
|
||||
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||
}
|
||||
|
||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
@@ -57,17 +124,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (MG_State::pEGLContext->IsCurrentContextOpenGLCoreProfile() && vao && vao->GetExternalIndex() == 0) {
|
||||
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
@@ -75,6 +133,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// While transform feedback is active the draw's primitive type must match
|
||||
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
||||
// the constraint moves to the shader's output primitive type instead, so
|
||||
// the draw mode itself is unconstrained here.
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||
Bool compatible = false;
|
||||
switch (feedbackMode) {
|
||||
case GL_POINTS:
|
||||
compatible = mode == GL_POINTS;
|
||||
break;
|
||||
case GL_LINES:
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!compatible) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode is incompatible with the active transform feedback primitive mode."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -402,12 +492,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -444,7 +536,133 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
|
||||
return;
|
||||
}
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||
return;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"No program with transform feedback varyings is active."));
|
||||
return;
|
||||
}
|
||||
// Every capture buffer slot the program's mode uses must have a buffer bound.
|
||||
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
if (point.GetBoundObject() == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
}
|
||||
|
||||
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
|
||||
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
|
||||
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
|
||||
// lengths the captured records are reordered in place: swap the first two
|
||||
// vertex records of every odd triangle within each emitted strip.
|
||||
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
|
||||
Uint64 inputPrimitives) {
|
||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& stripTriangles = program->GetGsStripTriangles();
|
||||
|
||||
// Global triangle indices whose leading vertex pair must swap.
|
||||
Vector<Uint64> swapTriangles;
|
||||
Uint64 triangleBase = 0;
|
||||
for (Uint64 input = 0; input < inputPrimitives; ++input) {
|
||||
for (const Uint32 stripLength : stripTriangles) {
|
||||
for (Uint32 t = 1; t < stripLength; t += 2) {
|
||||
swapTriangles.push_back(triangleBase + t);
|
||||
}
|
||||
triangleBase += stripLength;
|
||||
}
|
||||
}
|
||||
if (swapTriangles.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
|
||||
if (stride == 0) continue;
|
||||
const auto& bindingPoint =
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(bufferIndex));
|
||||
const auto& buffer = bindingPoint.GetBoundObject();
|
||||
if (buffer == nullptr) continue;
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
// The geometry stage amplifies, so the CPU vertex counter does not bound
|
||||
// the capture; the binding range's whole-triangle capacity does.
|
||||
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
|
||||
|
||||
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
|
||||
// (winding preserved by swapping the trailing pair); GL wants
|
||||
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
|
||||
Vector<Uint8> scratch(stride);
|
||||
for (const Uint64 triangle : swapTriangles) {
|
||||
if (triangle >= capturedTriangles) break;
|
||||
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
|
||||
const SizeT v1Offset = v0Offset + stride;
|
||||
const SizeT v2Offset = v1Offset + stride;
|
||||
Memcpy(scratch.data(), mapped + v2Offset, stride);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
|
||||
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EndTransformFeedback(void) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
|
||||
return;
|
||||
}
|
||||
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||
// End; the capture targets are host-coherent GPU memory, so completing
|
||||
// the GPU work is all that is required.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback(void);
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
|
||||
@@ -236,12 +236,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
|
||||
@@ -295,7 +295,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
|
||||
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
|
||||
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
|
||||
// is both truthful and what the spec requires for a name that was never generated.
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
||||
@@ -418,7 +424,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
@@ -998,8 +1004,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||
DECLARE_GL_FUNCTION_STUB_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_STUB_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, 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, 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)
|
||||
@@ -1063,7 +1069,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture,
|
||||
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_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, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_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)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
|
||||
@@ -2583,7 +2589,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
|
||||
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(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
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, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
|
||||
|
||||
@@ -45,10 +45,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
|
||||
}
|
||||
|
||||
// Mirrors the renderer-side gate: distinct depth/stencil renderbuffers (or a
|
||||
// renderbuffer paired with a texture) cannot form one Vulkan depth-stencil
|
||||
// attachment, and GL permits reporting such framebuffers as UNSUPPORTED.
|
||||
Bool HasDistinctCompleteDepthStencilRenderbufferAttachments(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
if (framebufferObject.GetExternalIndex() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) {
|
||||
return false;
|
||||
}
|
||||
if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) {
|
||||
return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get();
|
||||
}
|
||||
return (depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) &&
|
||||
(depthAttachment.IsTexture() || stencilAttachment.IsTexture());
|
||||
}
|
||||
|
||||
Bool IsUnsupportedFramebufferForDirectVulkan(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject);
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) ||
|
||||
HasDistinctCompleteDepthStencilRenderbufferAttachments(framebufferObject);
|
||||
}
|
||||
|
||||
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
@@ -60,12 +82,235 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the backend can actually attach this color format to a framebuffer. Preferred
|
||||
// source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus
|
||||
// probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend
|
||||
// cannot answer for fall back to a conservative static list of formats no ES driver renders to:
|
||||
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
|
||||
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
|
||||
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
||||
const SizeT sentinelFormat = static_cast<SizeT>(TextureInternalFormat::RGBA8);
|
||||
Bool cachePopulated = false;
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated;
|
||||
++targetIndex) {
|
||||
cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat],
|
||||
MG_Backend::FormatCapability::Creatable);
|
||||
}
|
||||
if (cachePopulated) {
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
++targetIndex) {
|
||||
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
||||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
case TextureInternalFormat::RGB16:
|
||||
case TextureInternalFormat::RGB10: // stored as RGB16
|
||||
case TextureInternalFormat::RGB12: // stored as RGB16
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
const auto& attachments = framebufferObject.GetAllAttachmentObjects();
|
||||
for (SizeT i = 0; i < attachments.size(); ++i) {
|
||||
const auto type = static_cast<FramebufferAttachmentType>(i);
|
||||
if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) {
|
||||
continue;
|
||||
}
|
||||
const auto& attachment = attachments[i];
|
||||
if (!attachment.IsValid()) continue;
|
||||
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
||||
if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
format = attachment.GetTexture()->GetFormat();
|
||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
|
||||
}
|
||||
|
||||
GLint ClassifyAttachmentComponentType(TextureInternalFormat internalFormat,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// A stencil value is an unsigned integer index regardless of the depth half
|
||||
// of a packed format.
|
||||
if (attachmentType == FramebufferAttachmentType::Stencil) return GL_UNSIGNED_INT;
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return GL_FLOAT;
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return GL_INT;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return GL_UNSIGNED_INT;
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return GL_SIGNED_NORMALIZED;
|
||||
case TextureInternalFormat::Unknown:
|
||||
return GL_NONE;
|
||||
default:
|
||||
return GL_UNSIGNED_NORMALIZED;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
|
||||
// and its DSA variant. Returns true when pname was one of them.
|
||||
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
|
||||
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
|
||||
GLenum pname, GLint* params, const char* caller) {
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
|
||||
// The depth and stencil components have different types, so the combined
|
||||
// attachment name has no single answer.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"No image is attached to the queried attachment point."));
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
if (attachmentObject->IsTexture() && attachmentObject->GetTexture()) {
|
||||
internalFormat = attachmentObject->GetTexture()->GetFormat();
|
||||
} else if (attachmentObject->IsRenderbuffer() && attachmentObject->GetRenderbuffer()) {
|
||||
internalFormat = attachmentObject->GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
const auto sizes = MG_Util::GetComponentSizesForInternalFormat(internalFormat);
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
*params = sizes.Red;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
*params = sizes.Green;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
*params = sizes.Blue;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
*params = sizes.Alpha;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
*params = sizes.Depth;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
*params = sizes.Stencil;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
*params = (internalFormat == TextureInternalFormat::SRGB8 ||
|
||||
internalFormat == TextureInternalFormat::SRGB8Alpha8)
|
||||
? GL_SRGB
|
||||
: GL_LINEAR;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
*params = ClassifyAttachmentComponentType(internalFormat, attachmentType);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
|
||||
TextureUploadTarget& outUploadTarget,
|
||||
Bool& outLayered) {
|
||||
@@ -331,6 +576,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
|
||||
// Default-framebuffer attachment names (GL_DEPTH, GL_STENCIL, GL_FRONT/GL_BACK
|
||||
// variants) alias onto the equivalent attachment points.
|
||||
switch (attachment) {
|
||||
case GL_DEPTH:
|
||||
attachment = GL_DEPTH_ATTACHMENT;
|
||||
break;
|
||||
case GL_STENCIL:
|
||||
attachment = GL_STENCIL_ATTACHMENT;
|
||||
break;
|
||||
case GL_FRONT:
|
||||
case GL_FRONT_LEFT:
|
||||
case GL_FRONT_RIGHT:
|
||||
case GL_BACK:
|
||||
case GL_BACK_LEFT:
|
||||
case GL_BACK_RIGHT:
|
||||
attachment = GL_COLOR_ATTACHMENT0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
|
||||
FramebufferAttachmentType attachmentType = depthStencilAlias
|
||||
? FramebufferAttachmentType::Depth
|
||||
@@ -347,20 +613,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -424,6 +710,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
"GetFramebufferAttachmentParameteriv_State")) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -556,6 +846,62 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return framebufferObject;
|
||||
}
|
||||
|
||||
// Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer
|
||||
// index; the DirectGLES backend attaches it with glFramebufferTextureLayer.
|
||||
static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level, GLint layer,
|
||||
TextureUploadTarget textureUploadTarget) {
|
||||
if (target == GL_FRAMEBUFFER) {
|
||||
target = GL_DRAW_FRAMEBUFFER;
|
||||
}
|
||||
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer,
|
||||
textureUploadTarget);
|
||||
AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer,
|
||||
textureUploadTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (layer < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Layer must be non-negative."));
|
||||
return;
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
}
|
||||
|
||||
void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
|
||||
if (texture == 0) {
|
||||
const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D;
|
||||
@@ -563,10 +909,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
static_cast<void>(layer);
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__,
|
||||
"Layered framebuffer texture attachments are not represented by the current framebuffer attachment model.");
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
|
||||
switch (textureObject->GetTarget()) {
|
||||
case TextureTarget::Texture3D:
|
||||
textureUploadTarget = TextureUploadTarget::Texture3D;
|
||||
break;
|
||||
case TextureTarget::Texture2DArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture2DArray;
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
|
||||
break;
|
||||
default:
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
|
||||
return;
|
||||
}
|
||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
|
||||
}
|
||||
|
||||
void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
|
||||
@@ -578,10 +945,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
static_cast<void>(zoffset);
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__,
|
||||
"3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model.");
|
||||
if (textarget != GL_TEXTURE_3D) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"FramebufferTexture3D requires GL_TEXTURE_3D."));
|
||||
return;
|
||||
}
|
||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset,
|
||||
TextureUploadTarget::Texture3D);
|
||||
}
|
||||
|
||||
void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
|
||||
@@ -1014,7 +1386,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
|
||||
auto& fbo = bindingSlot.GetBoundObject();
|
||||
const bool isDefaultFBO = (fbo == FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
|
||||
static GLenum bufs[] = {buf};
|
||||
const GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(fbo, isDefaultFBO, 1, bufs, true);
|
||||
}
|
||||
}
|
||||
@@ -1201,7 +1573,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = renderbuffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue;
|
||||
// GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue;
|
||||
MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
@@ -1224,7 +1598,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = framebuffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue;
|
||||
// GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue;
|
||||
MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
@@ -1252,6 +1628,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
|
||||
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
|
||||
}
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
@@ -1277,6 +1656,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
|
||||
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
|
||||
}
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
@@ -1295,20 +1677,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -1354,6 +1756,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
caller)) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1626,8 +2032,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check framebuffer completeness
|
||||
if (!framebufferObject->CheckCompleteness()) {
|
||||
// Check framebuffer completeness (including formats the ES pipeline cannot attach)
|
||||
if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidFramebufferOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete"));
|
||||
@@ -1679,6 +2085,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"No color buffer for color format"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read
|
||||
// buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error
|
||||
// for every *_INTEGER readback from a normalized attachment).
|
||||
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
|
||||
TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown;
|
||||
if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
|
||||
attachmentFormat = readAttachment.GetTexture()->GetFormat();
|
||||
} else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
|
||||
attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
if (attachmentFormat != TextureInternalFormat::Unknown &&
|
||||
TextureImpl::IsIntegerColorInputFormat(textureInputFormat) !=
|
||||
TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State",
|
||||
"Integer-ness of format does not match the read buffer"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
|
||||
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
|
||||
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
|
||||
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
|
||||
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
|
||||
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
|
||||
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check PBO state
|
||||
@@ -1937,6 +2377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
namespace FramebufferImpl {
|
||||
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
|
||||
} // namespace FramebufferImpl
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
|
||||
};
|
||||
|
||||
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
|
||||
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
|
||||
} // namespace FramebufferImpl
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -76,7 +76,13 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
|
||||
if (index == 0 && !allowZero) {
|
||||
if (index == 0) {
|
||||
// Zero is never a GenRenderbuffers name, so it must not reach the name-table lookup
|
||||
// below: where it is allowed (glBindRenderbuffer / FramebufferRenderbuffer detach) it
|
||||
// means "unbind", and looking it up would record a bogus INVALID_OPERATION - GL CTS's
|
||||
// per-case state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) after every case.
|
||||
if (allowZero) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
|
||||
|
||||
@@ -213,8 +213,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
@@ -465,6 +470,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_STENCIL_TEST:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -520,6 +533,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
|
||||
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
|
||||
} else {
|
||||
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_CLEAR_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetClearDepth();
|
||||
return;
|
||||
@@ -529,6 +555,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.AliasedLineWidthRangeMax;
|
||||
return;
|
||||
}
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
|
||||
// EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
|
||||
// from here, so this case is the authoritative one.
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
|
||||
return;
|
||||
}
|
||||
case GL_ALIASED_POINT_SIZE_RANGE:
|
||||
case GL_POINT_SIZE_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
@@ -1168,10 +1201,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentInputComponents;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||
// TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap.
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms;
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxFragmentUniformComponents;
|
||||
@@ -1201,7 +1233,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxGeometryTextureImageUnits;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||
@@ -1270,7 +1304,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxVertexAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||
return;
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
@@ -1727,7 +1763,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 1024 * 1024; // TODO
|
||||
return;
|
||||
case GL_CONTEXT_PROFILE_MASK:
|
||||
*params = GL_CONTEXT_CORE_PROFILE_BIT;
|
||||
// Reports the requested context profile (EGL defaults 3.x contexts to core);
|
||||
// MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity.
|
||||
*params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile()
|
||||
? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT
|
||||
: GL_CONTEXT_CORE_PROFILE_BIT;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
@@ -1887,6 +1927,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateComponents;
|
||||
break;
|
||||
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
|
||||
// whether the extension is advertised and requires no GL error; desktop
|
||||
// drivers all accept them, so answer with the separate-attrib capacity and
|
||||
// the single vertex stream the backends provide.
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||
*params = 0;
|
||||
break;
|
||||
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
||||
*params = dynamicParameters.MaxTextureImageUnits;
|
||||
break;
|
||||
@@ -1894,7 +1950,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxTextureSize;
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
|
||||
*params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings);
|
||||
// Never advertise more bindings than the state layer's indexed-binding array can track
|
||||
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
|
||||
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
|
||||
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
|
||||
// (36), so the clamp never under-advertises.
|
||||
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
|
||||
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BLOCK_SIZE:
|
||||
*params = dynamicParameters.MaxUniformBlockSize;
|
||||
@@ -1937,6 +1999,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SUBPIXEL_BITS:
|
||||
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
|
||||
break;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
*params = dynamicParameters.FragmentInterpolationOffsetBits;
|
||||
break;
|
||||
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
break;
|
||||
@@ -1955,6 +2026,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
|
||||
break;
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
@@ -1972,4 +2047,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
|
||||
}
|
||||
|
||||
GLenum GetGraphicsResetStatus() {
|
||||
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
|
||||
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
|
||||
// device after every case (gl3cTestPackages.cpp:121) and, under the default
|
||||
// --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
|
||||
return GL_NO_ERROR;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -21,4 +21,5 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -49,10 +49,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckProgramNameValidity(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
// Programs and shaders share one name space: a name that exists but
|
||||
// belongs to a shader is INVALID_OPERATION, a name GL never handed
|
||||
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
|
||||
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) + " is not a valid name."));
|
||||
std::to_string(program) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a program object."
|
||||
: " is not a valid name.")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -339,6 +347,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program."));
|
||||
return;
|
||||
}
|
||||
// A shader flagged with glDeleteShader lives on while attached; this detach may
|
||||
// have been its last GL-visible attachment.
|
||||
MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader);
|
||||
}
|
||||
|
||||
void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size,
|
||||
@@ -602,6 +613,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
*params = programObject->GetTransformFeedbackVaryingMaxLength();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -621,9 +644,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
case GL_PROGRAM_BINARY_LENGTH:
|
||||
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
@@ -735,6 +755,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = (char*)programObject->MapUBO();
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
@@ -784,6 +810,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<T, GLfloat>) {
|
||||
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
|
||||
@@ -812,19 +844,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsProgram_State(GLuint program) {
|
||||
/* FIXME: Handle situations that:
|
||||
* A program object marked for deletion with glDeleteProgram but still in use as part of current
|
||||
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
|
||||
*/
|
||||
// Deletion-flagged names stay valid while the object is still GL-visible (program in
|
||||
// use, shader attached), so name validity is exactly the Is* answer.
|
||||
if (program == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
GLboolean IsShader_State(GLuint shader) {
|
||||
/* FIXME: Handle situations that:
|
||||
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
|
||||
* considered a shader object and glIsShader will return GL_TRUE.
|
||||
*/
|
||||
if (shader == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
@@ -834,6 +860,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject) return;
|
||||
MGLOG_D("%s: linking program %d", __func__, program);
|
||||
|
||||
// Relinking the program an active transform feedback captures from would
|
||||
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"The program used by active transform feedback cannot be relinked."));
|
||||
return;
|
||||
}
|
||||
|
||||
static Bool allowVSOnlyPrograms;
|
||||
static Bool initialized = false;
|
||||
if (!initialized) {
|
||||
@@ -877,6 +915,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void UseProgram_State(GLuint program) {
|
||||
MGLOG_D("UseProgram_State: program=%u", program);
|
||||
|
||||
// GL 3.3 core 2.11.3: the program in use may not change while transform
|
||||
// feedback is active (there is no pause in 3.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The current program cannot change while transform feedback is active."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (program == 0) {
|
||||
MG_State::pGLContext->UseProgram(0);
|
||||
return;
|
||||
@@ -900,14 +948,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, programObject.GetMaxUniformLocation());
|
||||
auto size = programObject.GetUniformSizesInBytes(location);
|
||||
auto offset = programObject.GetUniformOffset(location);
|
||||
MOBILEGL_ASSERT(size >= ItemCount * sizeof(T),
|
||||
"Uniform size mismatch, expected at least %zu bytes, got %zu bytes.", ItemCount * sizeof(T),
|
||||
size);
|
||||
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
||||
const Uint offset = programObject.GetUniformOffset(location);
|
||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||
const SizeT uboSize = programObject.GetUBOSize();
|
||||
SizeT writeSize = ItemCount * sizeof(T);
|
||||
if (size < writeSize) {
|
||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
"bytes; clamping",
|
||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||
writeSize = size;
|
||||
}
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||
// Should not happen: linking gives every settable uniform backing
|
||||
// storage. Log and drop the write instead of faulting.
|
||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
"uboSize=%zu); dropping write",
|
||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||
writeSize, uboSize);
|
||||
return;
|
||||
}
|
||||
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, offset + byteOffsetInsideUniform);
|
||||
Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T));
|
||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||
programObject.MarkUBOContentDirty();
|
||||
} else {
|
||||
auto* ttype = programObject.GetUniformTType(location);
|
||||
@@ -940,6 +1005,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
// GL 3.3 §2.11.4: values for elements beyond the end of the uniform
|
||||
// array are ignored. Never step onto a neighboring uniform's location.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + offset)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + offset, "the current program object");
|
||||
return;
|
||||
@@ -964,6 +1034,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + offset)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + offset,
|
||||
"program " + std::to_string(program));
|
||||
@@ -1092,6 +1166,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1124,6 +1202,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
// Handle padding in mat3 correctly!!
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1159,6 +1241,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1219,6 +1305,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1249,6 +1339,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1283,6 +1377,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1341,6 +1439,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
|
||||
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -1364,6 +1463,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(program) + "."));
|
||||
return;
|
||||
}
|
||||
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
||||
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
|
||||
}
|
||||
|
||||
@@ -1435,9 +1535,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
||||
// Member entries of an arrayed block are recorded against the first instance;
|
||||
// every instance of the array reports that shared member set (matches
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
|
||||
GLint uniformIndexCount = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast<Int>(uniformBlockIndex)) {
|
||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
|
||||
continue;
|
||||
}
|
||||
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
||||
@@ -2146,4 +2250,66 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ValidateProgram(GLuint program) {
|
||||
ValidateProgram_State(program);
|
||||
}
|
||||
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
|
||||
return;
|
||||
}
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
|
||||
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
|
||||
return;
|
||||
}
|
||||
Vector<String> names;
|
||||
names.reserve(static_cast<SizeT>(count));
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
|
||||
}
|
||||
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
|
||||
}
|
||||
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) + " has not been successfully linked."));
|
||||
return;
|
||||
}
|
||||
const auto* varying = programObject->GetTransformFeedbackVarying(index);
|
||||
if (varying == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"index is not an active transform feedback varying of the program."));
|
||||
return;
|
||||
}
|
||||
if (size != nullptr) *size = varying->size;
|
||||
if (type != nullptr) *type = varying->type;
|
||||
GLsizei written = 0;
|
||||
if (name != nullptr && bufSize > 0) {
|
||||
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
|
||||
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
|
||||
name[written] = '\0';
|
||||
}
|
||||
if (length != nullptr) *length = written;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -138,4 +138,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
void ValidateProgram(GLuint program);
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Bool ended = false;
|
||||
Bool resultCached = false;
|
||||
Uint64 cachedResult = 0;
|
||||
// Transform feedback primitive counter at BeginQuery time.
|
||||
Uint64 counterSnapshot = 0;
|
||||
};
|
||||
|
||||
// Query calls may arrive from any thread (launchers migrate the context
|
||||
@@ -41,6 +43,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_nextQueryId = 1;
|
||||
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
||||
GLuint g_activeTimeElapsedQueryId = 0;
|
||||
// Ids of the queries active on the transform feedback targets (0 = none).
|
||||
GLuint g_activePrimitivesWrittenQueryId = 0;
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -126,6 +133,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
outValue = 0;
|
||||
return true;
|
||||
}
|
||||
// ANY_SAMPLES_PASSED* report a boolean.
|
||||
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
result = result != 0 ? 1 : 0;
|
||||
}
|
||||
// Final value produced (or no GetQueryResult64 hook: the
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
@@ -180,7 +192,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
QueryObject* queryObject = it->second;
|
||||
if (queryObject->active) {
|
||||
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
|
||||
// Implicitly end before deletion, releasing the matching active slot.
|
||||
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId) = 0;
|
||||
} else {
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
}
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
@@ -204,10 +233,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BeginQuery(GLenum target, GLuint id) {
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
|
||||
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
|
||||
// BeginQuery target either.
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
@@ -221,9 +255,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
if (g_activeTimeElapsedQueryId != 0) {
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on GL_TIME_ELAPSED.");
|
||||
"A query is already active on this target.");
|
||||
return;
|
||||
}
|
||||
if (queryObject->active) {
|
||||
@@ -239,25 +277,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
g_activeTimeElapsedQueryId = id;
|
||||
if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||
} else if (isOcclusionQuery) {
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
activeQueryId = id;
|
||||
}
|
||||
|
||||
void EndQuery(GLenum target) {
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
if (g_activeTimeElapsedQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
return;
|
||||
}
|
||||
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
|
||||
auto* queryObject = FindQueryObjectLocked(activeQueryId);
|
||||
if (!queryObject) {
|
||||
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
// Result comes from the GPU query at read time.
|
||||
} else {
|
||||
queryObject->cachedResult =
|
||||
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
||||
queryObject->resultCached = true;
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
@@ -303,9 +388,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
switch (pname) {
|
||||
case GL_CURRENT_QUERY: {
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
|
||||
// never are, and other targets remain unimplemented.
|
||||
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
|
||||
switch (target) {
|
||||
case GL_TIME_ELAPSED:
|
||||
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
|
||||
break;
|
||||
case GL_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
|
||||
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
|
||||
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
|
||||
break;
|
||||
case GL_PRIMITIVES_GENERATED:
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GL_QUERY_COUNTER_BITS: {
|
||||
@@ -313,7 +414,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins. Non-timer targets remain unimplemented and report 0.
|
||||
// wins.
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||
return;
|
||||
}
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
const Bool supported =
|
||||
|
||||
@@ -649,7 +649,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ClearDepth_State(GLclampd depth) {
|
||||
MG_State::pGLContext->SetClearDepth(static_cast<Float>(depth));
|
||||
// GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear
|
||||
// values additionally require it: VUID-VkClearDepthStencilValue-depth-00022).
|
||||
MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast<Float>(depth)));
|
||||
}
|
||||
|
||||
void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isInteger) {
|
||||
Float ReadSamplerScalar(const void* param, Bool isFloat, Bool isUnsignedInteger) {
|
||||
if (isFloat) return *(const GLfloat*)param;
|
||||
if (isUnsignedInteger) return static_cast<Float>(*(const GLuint*)param);
|
||||
return static_cast<Float>(*(const GLint*)param);
|
||||
}
|
||||
|
||||
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
|
||||
if (param == nullptr) return false;
|
||||
|
||||
switch (pname) {
|
||||
@@ -22,6 +28,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_MAX_LOD:
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
return true;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -29,14 +42,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isFloat) {
|
||||
return SamplerImpl::ValidateSamplerFloatParam(pname, *(const GLfloat*)param);
|
||||
}
|
||||
if (isInteger) {
|
||||
if (isUnsignedInteger) {
|
||||
return SamplerImpl::ValidateSamplerIntParam(pname, static_cast<GLint>(*(const GLuint*)param));
|
||||
}
|
||||
return SamplerImpl::ValidateSamplerIntParam(pname, *(const GLint*)param);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, bool isInteger) {
|
||||
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
if (param == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -47,7 +61,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
auto& samplerObj = MG_State::pGLContext->GetSamplerObject(sampler);
|
||||
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
|
||||
if (!ValidateSamplerParameterValue(pname, param, isFloat, isInteger)) return;
|
||||
if (!ValidateSamplerParameterValue(pname, param, isFloat, isUnsignedInteger)) return;
|
||||
|
||||
using namespace MG_Util;
|
||||
switch (pname) {
|
||||
@@ -76,6 +90,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
samplerObj->SetLodBias(*(const GLfloat*)param);
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
samplerObj->SetMaxAnisotropy(ReadSamplerScalar(param, isFloat, isUnsignedInteger));
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
samplerObj->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(*(const GLint*)param));
|
||||
break;
|
||||
@@ -89,7 +106,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, bool isInteger) {
|
||||
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
if (params == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -129,6 +147,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
*(GLfloat*)params = samplerObj->GetLodBias();
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
|
||||
break;
|
||||
@@ -158,6 +185,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static thread_local Vector<GLuint> names;
|
||||
MG_State::pGLContext->GenSamplerNames(count, names);
|
||||
Memcpy(samplers, names.data(), count * sizeof(GLuint));
|
||||
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
|
||||
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->CreateSamplerObject(names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
|
||||
@@ -206,7 +238,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sampler == 0) {
|
||||
textureUnit.SetSamplerObject(nullptr);
|
||||
} else {
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
|
||||
// name, which is why this cannot go through the shared SamplerImpl validator.
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return;
|
||||
}
|
||||
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
|
||||
if (!doesSamplerObjectCreated) {
|
||||
MG_State::pGLContext->CreateSamplerObject(sampler);
|
||||
@@ -240,7 +281,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, true);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
@@ -268,7 +309,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, true);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
}
|
||||
|
||||
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
|
||||
|
||||
@@ -99,6 +99,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
return true;
|
||||
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (!(param >= 1.0f)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerFloatParam",
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
case GL_TEXTURE_BORDER_COLOR:
|
||||
if (param < 0.0f || param > 1.0f) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -125,6 +135,16 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
}
|
||||
return true;
|
||||
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (param < 1) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerIntParam",
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return ValidateSamplerParam(pname, static_cast<GLenum>(param));
|
||||
}
|
||||
|
||||
@@ -133,4 +133,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
values[0] = value;
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyAllSyncObjects() {
|
||||
// Detach the registry under the lock, release outside it. Entries the app
|
||||
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
||||
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
||||
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
||||
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
||||
// race an app-driven DeleteSync already has.
|
||||
UnorderedMap<GLsync, SyncObject*> orphans;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
||||
orphans.swap(g_liveSyncObjects);
|
||||
}
|
||||
if (orphans.empty()) {
|
||||
return;
|
||||
}
|
||||
// Both backends' DeleteSync only free the heap wrapper once their GL
|
||||
// context/renderer is gone (generation/current-thread guards), so this is
|
||||
// safe after the backend has released its EGL resources - but not after
|
||||
// the function table itself is cleared.
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
}
|
||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -16,4 +16,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||
void DeleteSync(GLsync sync);
|
||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
||||
// Destroys every still-registered sync object exactly as DeleteSync would.
|
||||
// GL requires syncs 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.
|
||||
void DestroyAllSyncObjects();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,9 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
|
||||
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);
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -95,6 +98,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLsizei width, GLsizei height);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height);
|
||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
UniquePtr<ProxyTextureManager> pProxyTextureManager;
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
|
||||
|
||||
Bool IsProxyTextureTarget(TextureUploadTarget target) {
|
||||
switch (target) {
|
||||
|
||||
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
|
||||
};
|
||||
|
||||
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
|
||||
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -175,13 +175,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsIntegerColorInputFormat(TextureInputFormat format) {
|
||||
Bool IsIntegerColorInputFormat(TextureInputFormat format) {
|
||||
return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger ||
|
||||
format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger ||
|
||||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger;
|
||||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger ||
|
||||
format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger ||
|
||||
format == TextureInputFormat::AlphaInteger;
|
||||
}
|
||||
|
||||
static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
|
||||
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R8UI:
|
||||
@@ -235,17 +237,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
format == TextureInputFormat::StencilIndex;
|
||||
}
|
||||
|
||||
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
|
||||
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
|
||||
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
|
||||
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type) {
|
||||
// Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks
|
||||
// (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels
|
||||
// (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and
|
||||
// integer formats reject floating-point types; violations raise GL_INVALID_OPERATION.
|
||||
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) {
|
||||
const auto recordInvalidOperation = [](const char* message) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
|
||||
message));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateClientFormatTypePairing", message));
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -288,6 +288,27 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return recordInvalidOperation("Integer format cannot be used with a floating-point type");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
|
||||
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
|
||||
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
|
||||
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type) {
|
||||
const auto recordInvalidOperation = [](const char* message) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
|
||||
message));
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!ValidateClientFormatTypePairing(format, type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
|
||||
if (format == TextureInputFormat::StencilIndex) {
|
||||
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
|
||||
@@ -339,6 +360,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller) {
|
||||
if (textureObject && textureObject->GetExternalIndex() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"This operation is not allowed on the default texture (zero is "
|
||||
"bound to the target)."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
TextureTarget target) {
|
||||
if (!textureObject) return true; // should be created later
|
||||
|
||||
@@ -23,11 +23,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
|
||||
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
|
||||
Bool ValidateTextureBorderNumber(Int border);
|
||||
Bool IsIntegerColorInputFormat(TextureInputFormat format);
|
||||
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat);
|
||||
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type);
|
||||
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
// error is generated if zero is bound to target", ARB_texture_storage).
|
||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller);
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
||||
|
||||
@@ -78,15 +78,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) {
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false;
|
||||
if (index == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"Generic vertex attribute 0 current value cannot be modified."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// GL 3.3 core 2.7: VertexAttrib* sets the current value of ANY generic attribute,
|
||||
// including index 0 - only an out-of-range index is an error (INVALID_VALUE).
|
||||
// "Attribute 0 is immutable" was legacy immediate-mode lore; rejecting it broke GL
|
||||
// CTS's per-case state reset, which writes vertexAttrib4f(0, 0,0,0,1) after every case.
|
||||
static_cast<void>(funcName);
|
||||
return VertexArrayImpl::ValidateVertexAttributeIndex(index);
|
||||
}
|
||||
|
||||
static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) {
|
||||
@@ -288,7 +285,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint vao = arrays[i];
|
||||
if (vao == 0) continue;
|
||||
|
||||
if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue;
|
||||
// GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue;
|
||||
|
||||
if (MG_State::pGLContext->GetBoundVertexArray() &&
|
||||
MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) {
|
||||
|
||||
@@ -85,6 +85,8 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(CGLGetPixelFormat, name);
|
||||
GETPROC(CGLSetCurrentContext, name);
|
||||
GETPROC(CGLGetCurrentContext, name);
|
||||
GETPROC(CGLSetVirtualScreen, name);
|
||||
GETPROC(CGLGetVirtualScreen, name);
|
||||
GETPROC(CGLSetParameter, name);
|
||||
GETPROC(CGLGetParameter, name);
|
||||
GETPROC(CGLUpdateContext, name);
|
||||
|
||||
@@ -29,10 +29,19 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
char kContextViewKey;
|
||||
char kContextLayerKey;
|
||||
|
||||
std::once_flag g_installOnce;
|
||||
IMP g_pixelFormatDealloc = nullptr;
|
||||
IMP g_contextDealloc = nullptr;
|
||||
|
||||
std::mutex& HookInstallMutex() {
|
||||
static auto* mutex = new std::mutex();
|
||||
return *mutex;
|
||||
}
|
||||
|
||||
Bool& HooksInstalled() {
|
||||
static auto* installed = new Bool(false);
|
||||
return *installed;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
Fn ObjcMsgSend() {
|
||||
return reinterpret_cast<Fn>(objc_msgSend);
|
||||
@@ -431,12 +440,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
method_setImplementation(method, replacement);
|
||||
}
|
||||
|
||||
void InstallHooksOnce() {
|
||||
Bool InstallHooksOnce() {
|
||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||
if (!pixelFormatClass || !contextClass) {
|
||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
|
||||
@@ -471,11 +480,34 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
|
||||
|
||||
MGLOG_I("NSOpenGLImpl hooks installed");
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void InstallHooks() {
|
||||
std::call_once(g_installOnce, InstallHooksOnce);
|
||||
const std::lock_guard<std::mutex> lock(HookInstallMutex());
|
||||
if (!HooksInstalled()) {
|
||||
// Do not permanently consume the install attempt when the OpenGL
|
||||
// framework has not registered its Objective-C classes yet. The
|
||||
// dyld bootstrap normally runs after framework dependencies, but
|
||||
// an explicitly loaded/static-linked MobileGL can arrive earlier.
|
||||
HooksInstalled() = InstallHooksOnce();
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
|
||||
|
||||
namespace {
|
||||
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
|
||||
// its first dlsym("glGetString") or other MobileGL host-API call. Install
|
||||
// only the lightweight Objective-C dispatch hooks while the injected dylib
|
||||
// is loading so those first Cocoa objects are routed through CGLImpl. The
|
||||
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
|
||||
// the full, thread-safe MobileGL initialization outside this bootstrap.
|
||||
//
|
||||
// There is intentionally no matching destructor: backend teardown remains
|
||||
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
|
||||
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
|
||||
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.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
|
||||
|
||||
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
|
||||
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
|
||||
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
|
||||
// be defined before the first windows.h inclusion in this translation unit.
|
||||
#if defined(_WIN32) && !defined(_GDI32_)
|
||||
#define _GDI32_ 1
|
||||
#endif
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "../WGLImpl.h"
|
||||
|
||||
namespace WGL = MobileGL::MG_Impl::WGLImpl;
|
||||
|
||||
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
|
||||
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
|
||||
|
||||
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
|
||||
return WGL::ChoosePixelFormat(hdc, ppfd);
|
||||
}
|
||||
|
||||
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
|
||||
LPPIXELFORMATDESCRIPTOR ppfd) {
|
||||
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
|
||||
}
|
||||
|
||||
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
|
||||
return WGL::GetPixelFormat(hdc);
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
|
||||
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
|
||||
return WGL::SwapBuffers(hdc);
|
||||
}
|
||||
|
||||
// ---- Context management ----
|
||||
|
||||
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
|
||||
return WGL::CreateContext(hdc);
|
||||
}
|
||||
|
||||
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
||||
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||
MGLOG_W("wglCopyContext is not supported");
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
|
||||
return WGL::DeleteContext(hglrc);
|
||||
}
|
||||
|
||||
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
|
||||
return WGL::GetCurrentContext();
|
||||
}
|
||||
|
||||
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
|
||||
return WGL::GetCurrentDC();
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
|
||||
return WGL::MakeCurrent(hdc, hglrc);
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
|
||||
return WGL::ShareLists(hglrcShare, hglrcDest);
|
||||
}
|
||||
|
||||
// ---- Proc address ----
|
||||
|
||||
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
|
||||
return WGL::GetProcAddress(lpszProc);
|
||||
}
|
||||
|
||||
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
|
||||
return WGL::GetProcAddress(lpszProc);
|
||||
}
|
||||
|
||||
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
|
||||
|
||||
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
|
||||
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
|
||||
return WGL::SwapBuffers(hdc);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
||||
if (!ps) {
|
||||
return 0;
|
||||
}
|
||||
DWORD swapped = 0;
|
||||
for (UINT i = 0; i < n; ++i) {
|
||||
if (WGL::SwapBuffers(ps[i].hdc)) {
|
||||
++swapped;
|
||||
}
|
||||
}
|
||||
return swapped;
|
||||
}
|
||||
|
||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -0,0 +1,30 @@
|
||||
; MobileGL WGL exports. The wgl* entry points are defined without
|
||||
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
|
||||
; become plain declarations, and MSVC rejects adding dllexport afterwards),
|
||||
; so this .def file is what actually exports them from the DLL.
|
||||
EXPORTS
|
||||
wglChoosePixelFormat
|
||||
wglCopyContext
|
||||
wglCreateContext
|
||||
wglCreateLayerContext
|
||||
wglDeleteContext
|
||||
wglDescribeLayerPlane
|
||||
wglDescribePixelFormat
|
||||
wglGetCurrentContext
|
||||
wglGetCurrentDC
|
||||
wglGetDefaultProcAddress
|
||||
wglGetLayerPaletteEntries
|
||||
wglGetPixelFormat
|
||||
wglGetProcAddress
|
||||
wglMakeCurrent
|
||||
wglRealizeLayerPalette
|
||||
wglSetLayerPaletteEntries
|
||||
wglSetPixelFormat
|
||||
wglShareLists
|
||||
wglSwapBuffers
|
||||
wglSwapLayerBuffers
|
||||
wglSwapMultipleBuffers
|
||||
wglUseFontBitmapsA
|
||||
wglUseFontBitmapsW
|
||||
wglUseFontOutlinesA
|
||||
wglUseFontOutlinesW
|
||||
@@ -0,0 +1,732 @@
|
||||
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.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 "WGLImpl.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "../EGLImpl/EGLImpl.h"
|
||||
#include "../GetProcAddress.h"
|
||||
#include <Init.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::WGLImpl {
|
||||
namespace {
|
||||
// WGL_ARB_pixel_format
|
||||
constexpr int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000;
|
||||
constexpr int WGL_DRAW_TO_WINDOW_ARB = 0x2001;
|
||||
constexpr int WGL_DRAW_TO_BITMAP_ARB = 0x2002;
|
||||
constexpr int WGL_ACCELERATION_ARB = 0x2003;
|
||||
constexpr int WGL_NEED_PALETTE_ARB = 0x2004;
|
||||
constexpr int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005;
|
||||
constexpr int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006;
|
||||
constexpr int WGL_SWAP_METHOD_ARB = 0x2007;
|
||||
constexpr int WGL_NUMBER_OVERLAYS_ARB = 0x2008;
|
||||
constexpr int WGL_NUMBER_UNDERLAYS_ARB = 0x2009;
|
||||
constexpr int WGL_TRANSPARENT_ARB = 0x200A;
|
||||
constexpr int WGL_SHARE_DEPTH_ARB = 0x200C;
|
||||
constexpr int WGL_SHARE_STENCIL_ARB = 0x200D;
|
||||
constexpr int WGL_SHARE_ACCUM_ARB = 0x200E;
|
||||
constexpr int WGL_SUPPORT_GDI_ARB = 0x200F;
|
||||
constexpr int WGL_SUPPORT_OPENGL_ARB = 0x2010;
|
||||
constexpr int WGL_DOUBLE_BUFFER_ARB = 0x2011;
|
||||
constexpr int WGL_STEREO_ARB = 0x2012;
|
||||
constexpr int WGL_PIXEL_TYPE_ARB = 0x2013;
|
||||
constexpr int WGL_COLOR_BITS_ARB = 0x2014;
|
||||
constexpr int WGL_RED_BITS_ARB = 0x2015;
|
||||
constexpr int WGL_RED_SHIFT_ARB = 0x2016;
|
||||
constexpr int WGL_GREEN_BITS_ARB = 0x2017;
|
||||
constexpr int WGL_GREEN_SHIFT_ARB = 0x2018;
|
||||
constexpr int WGL_BLUE_BITS_ARB = 0x2019;
|
||||
constexpr int WGL_BLUE_SHIFT_ARB = 0x201A;
|
||||
constexpr int WGL_ALPHA_BITS_ARB = 0x201B;
|
||||
constexpr int WGL_ALPHA_SHIFT_ARB = 0x201C;
|
||||
constexpr int WGL_ACCUM_BITS_ARB = 0x201D;
|
||||
constexpr int WGL_ACCUM_RED_BITS_ARB = 0x201E;
|
||||
constexpr int WGL_ACCUM_GREEN_BITS_ARB = 0x201F;
|
||||
constexpr int WGL_ACCUM_BLUE_BITS_ARB = 0x2020;
|
||||
constexpr int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021;
|
||||
constexpr int WGL_DEPTH_BITS_ARB = 0x2022;
|
||||
constexpr int WGL_STENCIL_BITS_ARB = 0x2023;
|
||||
constexpr int WGL_AUX_BUFFERS_ARB = 0x2024;
|
||||
constexpr int WGL_NO_ACCELERATION_ARB = 0x2025;
|
||||
constexpr int WGL_FULL_ACCELERATION_ARB = 0x2027;
|
||||
constexpr int WGL_SWAP_EXCHANGE_ARB = 0x2028;
|
||||
constexpr int WGL_TYPE_RGBA_ARB = 0x202B;
|
||||
// WGL_ARB_multisample
|
||||
constexpr int WGL_SAMPLE_BUFFERS_ARB = 0x2041;
|
||||
constexpr int WGL_SAMPLES_ARB = 0x2042;
|
||||
// WGL_ARB_create_context / _profile / _no_error
|
||||
constexpr int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
|
||||
constexpr int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
|
||||
constexpr int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093;
|
||||
constexpr int WGL_CONTEXT_FLAGS_ARB = 0x2094;
|
||||
constexpr int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
|
||||
constexpr int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
|
||||
constexpr int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
|
||||
constexpr int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
|
||||
constexpr int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
|
||||
constexpr int WGL_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
|
||||
constexpr DWORD ERROR_INVALID_VERSION_ARB = 0x2095;
|
||||
constexpr DWORD ERROR_INVALID_PROFILE_ARB = 0x2096;
|
||||
|
||||
struct PixelFormatInfo {
|
||||
GLint AlphaBits;
|
||||
GLint DepthBits;
|
||||
GLint StencilBits;
|
||||
};
|
||||
|
||||
// Mirrors the two EGLState configs (RGBA8 + depth24, stencil 8 / stencil 0).
|
||||
constexpr PixelFormatInfo kPixelFormats[] = {
|
||||
{8, 24, 8},
|
||||
{8, 24, 0},
|
||||
};
|
||||
constexpr int kPixelFormatCount = static_cast<int>(std::size(kPixelFormats));
|
||||
|
||||
struct ContextObject {
|
||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||
EGLConfig Config = nullptr;
|
||||
EGLContext Context = EGL_NO_CONTEXT;
|
||||
};
|
||||
|
||||
struct WindowSurface {
|
||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||
EGLSurface Surface = EGL_NO_SURFACE;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
};
|
||||
|
||||
std::recursive_mutex& RegistryMutex() {
|
||||
static auto* mutex = new std::recursive_mutex();
|
||||
return *mutex;
|
||||
}
|
||||
|
||||
UnorderedMap<HGLRC, ContextObject>& Contexts() {
|
||||
static auto* contexts = new UnorderedMap<HGLRC, ContextObject>();
|
||||
return *contexts;
|
||||
}
|
||||
|
||||
UnorderedMap<HWND, WindowSurface>& WindowSurfaces() {
|
||||
static auto* surfaces = new UnorderedMap<HWND, WindowSurface>();
|
||||
return *surfaces;
|
||||
}
|
||||
|
||||
UnorderedMap<HWND, int>& WindowPixelFormats() {
|
||||
static auto* formats = new UnorderedMap<HWND, int>();
|
||||
return *formats;
|
||||
}
|
||||
|
||||
Uint64& NextContextHandle() {
|
||||
static auto* handle = new Uint64(0x10000);
|
||||
return *handle;
|
||||
}
|
||||
|
||||
struct ThreadCurrent {
|
||||
HDC DC = nullptr;
|
||||
HGLRC Context = nullptr;
|
||||
};
|
||||
thread_local ThreadCurrent t_current;
|
||||
|
||||
Int& SwapIntervalShadow() {
|
||||
static auto* interval = new Int(1);
|
||||
return *interval;
|
||||
}
|
||||
|
||||
void EnsureInitialized() {
|
||||
// Initialize() loads backend libraries and glslang, which must not run
|
||||
// under the loader lock; first WGL call is the earliest safe moment.
|
||||
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
|
||||
// can follow a full teardown from the last eglTerminate.
|
||||
MobileGL::EnsureInitialized();
|
||||
}
|
||||
|
||||
EGLDisplay EnsureDisplay() {
|
||||
EnsureInitialized();
|
||||
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
return EGL_NO_DISPLAY;
|
||||
}
|
||||
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
|
||||
return EGL_NO_DISPLAY;
|
||||
}
|
||||
return display;
|
||||
}
|
||||
|
||||
HGLRC EncodeContext(Uint64 handle) {
|
||||
return reinterpret_cast<HGLRC>(static_cast<SizeT>(handle));
|
||||
}
|
||||
|
||||
ContextObject* TryGetContext(HGLRC hglrc) {
|
||||
auto& contexts = Contexts();
|
||||
auto it = contexts.find(hglrc);
|
||||
return it == contexts.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const PixelFormatInfo& PixelFormatForWindow(HWND hwnd) {
|
||||
auto& formats = WindowPixelFormats();
|
||||
auto it = formats.find(hwnd);
|
||||
int index = it == formats.end() ? 1 : it->second;
|
||||
if (index < 1 || index > kPixelFormatCount) {
|
||||
index = 1;
|
||||
}
|
||||
return kPixelFormats[index - 1];
|
||||
}
|
||||
|
||||
Bool QueryClientSize(HWND hwnd, Uint32& width, Uint32& height) {
|
||||
RECT rect{};
|
||||
if (!GetClientRect(hwnd, &rect)) {
|
||||
return false;
|
||||
}
|
||||
width = static_cast<Uint32>(std::max<LONG>(rect.right - rect.left, 1));
|
||||
height = static_cast<Uint32>(std::max<LONG>(rect.bottom - rect.top, 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
// The backends never query the HWND client size themselves; the WGL layer
|
||||
// owns size discovery and pushes changes through the internal resize hook
|
||||
// (same contract as the macOS CGL layer).
|
||||
void SyncSurfaceSize(HWND hwnd, WindowSurface& surface) {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryClientSize(hwnd, width, height)) {
|
||||
return;
|
||||
}
|
||||
if (width == surface.Width && height == surface.Height) {
|
||||
return;
|
||||
}
|
||||
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
|
||||
static_cast<EGLint>(width), static_cast<EGLint>(height))) {
|
||||
surface.Width = width;
|
||||
surface.Height = height;
|
||||
}
|
||||
}
|
||||
|
||||
WindowSurface* EnsureWindowSurface(HWND hwnd, const ContextObject& context) {
|
||||
auto& surfaces = WindowSurfaces();
|
||||
auto it = surfaces.find(hwnd);
|
||||
if (it != surfaces.end()) {
|
||||
SyncSurfaceSize(hwnd, it->second);
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryClientSize(hwnd, width, height)) {
|
||||
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const EGLAttrib attribs[] = {
|
||||
EGL_WIDTH, static_cast<EGLAttrib>(width),
|
||||
EGL_HEIGHT, static_cast<EGLAttrib>(height),
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLSurface surface =
|
||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WindowSurface record;
|
||||
record.Display = context.Display;
|
||||
record.Surface = surface;
|
||||
record.Width = width;
|
||||
record.Height = height;
|
||||
auto [inserted, _] = surfaces.emplace(hwnd, record);
|
||||
return &inserted->second;
|
||||
}
|
||||
|
||||
HGLRC CreateContextFromEGLAttribs(HDC hdc, HGLRC share, const EGLint* contextAttribs) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("wgl: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
|
||||
EGLContext shareContext = EGL_NO_CONTEXT;
|
||||
if (share) {
|
||||
auto* shareObject = TryGetContext(share);
|
||||
if (!shareObject) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return nullptr;
|
||||
}
|
||||
shareContext = shareObject->Context;
|
||||
}
|
||||
|
||||
HWND hwnd = WindowFromDC(hdc);
|
||||
const PixelFormatInfo& pixelFormat = PixelFormatForWindow(hwnd);
|
||||
const EGLint configAttribs[] = {
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, pixelFormat.AlphaBits,
|
||||
EGL_DEPTH_SIZE, pixelFormat.DepthBits,
|
||||
EGL_STENCIL_SIZE, pixelFormat.StencilBits,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLConfig config = nullptr;
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
||||
MGLOG_E("wgl: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("wgl: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ContextObject object;
|
||||
object.Display = display;
|
||||
object.Config = config;
|
||||
object.Context = eglContext;
|
||||
const auto handle = EncodeContext(NextContextHandle()++);
|
||||
Contexts()[handle] = object;
|
||||
MGLOG_I("wgl: created context %p (EGL context %p)", handle, eglContext);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// ---- WGL extension entry points (resolved via wglGetProcAddress only) ----
|
||||
|
||||
const char* WINAPI Ext_GetExtensionsStringARB(HDC) {
|
||||
return "WGL_ARB_create_context WGL_ARB_create_context_no_error WGL_ARB_create_context_profile "
|
||||
"WGL_ARB_extensions_string WGL_ARB_pixel_format WGL_EXT_extensions_string WGL_EXT_swap_control";
|
||||
}
|
||||
|
||||
const char* WINAPI Ext_GetExtensionsStringEXT() {
|
||||
return Ext_GetExtensionsStringARB(nullptr);
|
||||
}
|
||||
|
||||
HGLRC WINAPI Ext_CreateContextAttribsARB(HDC hdc, HGLRC hShareContext, const int* attribList) {
|
||||
EnsureInitialized();
|
||||
int major = 1;
|
||||
int minor = 0;
|
||||
int profileMask = 0;
|
||||
int flags = 0;
|
||||
if (attribList) {
|
||||
for (SizeT i = 0; attribList[i] != 0; i += 2) {
|
||||
const int attrib = attribList[i];
|
||||
const int value = attribList[i + 1];
|
||||
switch (attrib) {
|
||||
case WGL_CONTEXT_MAJOR_VERSION_ARB:
|
||||
major = value;
|
||||
break;
|
||||
case WGL_CONTEXT_MINOR_VERSION_ARB:
|
||||
minor = value;
|
||||
break;
|
||||
case WGL_CONTEXT_PROFILE_MASK_ARB:
|
||||
profileMask = value;
|
||||
break;
|
||||
case WGL_CONTEXT_FLAGS_ARB:
|
||||
flags = value;
|
||||
break;
|
||||
case WGL_CONTEXT_LAYER_PLANE_ARB:
|
||||
if (value != 0) {
|
||||
SetLastError(ERROR_INVALID_PARAMETER);
|
||||
return nullptr;
|
||||
}
|
||||
break;
|
||||
case WGL_CONTEXT_OPENGL_NO_ERROR_ARB:
|
||||
// Accepted and ignored: MobileGL always validates.
|
||||
break;
|
||||
default:
|
||||
MGLOG_D("wglCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (major < 1 || (profileMask & ~(WGL_CONTEXT_CORE_PROFILE_BIT_ARB |
|
||||
WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
|
||||
SetLastError(profileMask ? ERROR_INVALID_PROFILE_ARB : ERROR_INVALID_VERSION_ARB);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Vector<EGLint> attribs = {
|
||||
EGL_CONTEXT_MAJOR_VERSION, major,
|
||||
EGL_CONTEXT_MINOR_VERSION, minor,
|
||||
};
|
||||
const Bool wantsCompat = (profileMask & WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
|
||||
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
|
||||
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
|
||||
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
|
||||
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
|
||||
}
|
||||
if (flags & WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
|
||||
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
|
||||
attribs.push_back(EGL_TRUE);
|
||||
}
|
||||
if (flags & WGL_CONTEXT_DEBUG_BIT_ARB) {
|
||||
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
|
||||
attribs.push_back(EGL_TRUE);
|
||||
}
|
||||
attribs.push_back(EGL_NONE);
|
||||
|
||||
return CreateContextFromEGLAttribs(hdc, hShareContext, attribs.data());
|
||||
}
|
||||
|
||||
BOOL WINAPI Ext_SwapIntervalEXT(int interval) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
return FALSE;
|
||||
}
|
||||
if (interval < 0) {
|
||||
// Adaptive vsync is not supported; clamp to regular vsync.
|
||||
interval = 1;
|
||||
}
|
||||
EGLImpl::SwapInterval(display, interval);
|
||||
SwapIntervalShadow() = interval;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int WINAPI Ext_GetSwapIntervalEXT() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
return SwapIntervalShadow();
|
||||
}
|
||||
|
||||
int PixelFormatAttribValue(int format, int attrib) {
|
||||
const PixelFormatInfo& info = kPixelFormats[format - 1];
|
||||
switch (attrib) {
|
||||
case WGL_NUMBER_PIXEL_FORMATS_ARB:
|
||||
return kPixelFormatCount;
|
||||
case WGL_SUPPORT_OPENGL_ARB:
|
||||
case WGL_DRAW_TO_WINDOW_ARB:
|
||||
case WGL_DOUBLE_BUFFER_ARB:
|
||||
return 1;
|
||||
case WGL_ACCELERATION_ARB:
|
||||
return WGL_FULL_ACCELERATION_ARB;
|
||||
case WGL_PIXEL_TYPE_ARB:
|
||||
return WGL_TYPE_RGBA_ARB;
|
||||
case WGL_COLOR_BITS_ARB:
|
||||
return 32;
|
||||
case WGL_RED_BITS_ARB:
|
||||
case WGL_GREEN_BITS_ARB:
|
||||
case WGL_BLUE_BITS_ARB:
|
||||
return 8;
|
||||
case WGL_RED_SHIFT_ARB:
|
||||
return 16;
|
||||
case WGL_GREEN_SHIFT_ARB:
|
||||
return 8;
|
||||
case WGL_BLUE_SHIFT_ARB:
|
||||
return 0;
|
||||
case WGL_ALPHA_BITS_ARB:
|
||||
return info.AlphaBits;
|
||||
case WGL_ALPHA_SHIFT_ARB:
|
||||
return 24;
|
||||
case WGL_DEPTH_BITS_ARB:
|
||||
return info.DepthBits;
|
||||
case WGL_STENCIL_BITS_ARB:
|
||||
return info.StencilBits;
|
||||
case WGL_SWAP_METHOD_ARB:
|
||||
return WGL_SWAP_EXCHANGE_ARB;
|
||||
case WGL_DRAW_TO_BITMAP_ARB:
|
||||
case WGL_NEED_PALETTE_ARB:
|
||||
case WGL_NEED_SYSTEM_PALETTE_ARB:
|
||||
case WGL_SWAP_LAYER_BUFFERS_ARB:
|
||||
case WGL_NUMBER_OVERLAYS_ARB:
|
||||
case WGL_NUMBER_UNDERLAYS_ARB:
|
||||
case WGL_TRANSPARENT_ARB:
|
||||
case WGL_SHARE_DEPTH_ARB:
|
||||
case WGL_SHARE_STENCIL_ARB:
|
||||
case WGL_SHARE_ACCUM_ARB:
|
||||
case WGL_SUPPORT_GDI_ARB:
|
||||
case WGL_STEREO_ARB:
|
||||
case WGL_ACCUM_BITS_ARB:
|
||||
case WGL_ACCUM_RED_BITS_ARB:
|
||||
case WGL_ACCUM_GREEN_BITS_ARB:
|
||||
case WGL_ACCUM_BLUE_BITS_ARB:
|
||||
case WGL_ACCUM_ALPHA_BITS_ARB:
|
||||
case WGL_AUX_BUFFERS_ARB:
|
||||
case WGL_SAMPLE_BUFFERS_ARB:
|
||||
case WGL_SAMPLES_ARB:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL WINAPI Ext_GetPixelFormatAttribivARB(HDC, int iPixelFormat, int iLayerPlane, UINT nAttributes,
|
||||
const int* piAttributes, int* piValues) {
|
||||
if (iLayerPlane != 0 || !piAttributes || !piValues) {
|
||||
return FALSE;
|
||||
}
|
||||
// Format 0 is only valid for WGL_NUMBER_PIXEL_FORMATS_ARB queries.
|
||||
if (iPixelFormat < 0 || iPixelFormat > kPixelFormatCount) {
|
||||
return FALSE;
|
||||
}
|
||||
const int format = iPixelFormat == 0 ? 1 : iPixelFormat;
|
||||
for (UINT i = 0; i < nAttributes; ++i) {
|
||||
piValues[i] = PixelFormatAttribValue(format, piAttributes[i]);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL WINAPI Ext_GetPixelFormatAttribfvARB(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes,
|
||||
const int* piAttributes, FLOAT* pfValues) {
|
||||
if (!pfValues) {
|
||||
return FALSE;
|
||||
}
|
||||
Vector<int> values(nAttributes);
|
||||
if (!Ext_GetPixelFormatAttribivARB(hdc, iPixelFormat, iLayerPlane, nAttributes, piAttributes,
|
||||
values.data())) {
|
||||
return FALSE;
|
||||
}
|
||||
for (UINT i = 0; i < nAttributes; ++i) {
|
||||
pfValues[i] = static_cast<FLOAT>(values[i]);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL WINAPI Ext_ChoosePixelFormatARB(HDC, const int* piAttribIList, const FLOAT*, UINT nMaxFormats,
|
||||
int* piFormats, UINT* nNumFormats) {
|
||||
if (!piFormats || !nNumFormats) {
|
||||
return FALSE;
|
||||
}
|
||||
int wantedStencil = 0;
|
||||
if (piAttribIList) {
|
||||
for (SizeT i = 0; piAttribIList[i] != 0; i += 2) {
|
||||
if (piAttribIList[i] == WGL_STENCIL_BITS_ARB) {
|
||||
wantedStencil = piAttribIList[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
UINT count = 0;
|
||||
const int preferred = wantedStencil > 0 ? 1 : 2;
|
||||
const int fallback = wantedStencil > 0 ? 2 : 1;
|
||||
if (count < nMaxFormats) {
|
||||
piFormats[count++] = preferred;
|
||||
}
|
||||
if (count < nMaxFormats) {
|
||||
piFormats[count++] = fallback;
|
||||
}
|
||||
*nNumFormats = count;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
struct WGLExtensionProc {
|
||||
const char* Name;
|
||||
PROC Proc;
|
||||
};
|
||||
|
||||
const WGLExtensionProc kWGLExtensionProcs[] = {
|
||||
{"wglGetExtensionsStringARB", reinterpret_cast<PROC>(Ext_GetExtensionsStringARB)},
|
||||
{"wglGetExtensionsStringEXT", reinterpret_cast<PROC>(Ext_GetExtensionsStringEXT)},
|
||||
{"wglCreateContextAttribsARB", reinterpret_cast<PROC>(Ext_CreateContextAttribsARB)},
|
||||
{"wglSwapIntervalEXT", reinterpret_cast<PROC>(Ext_SwapIntervalEXT)},
|
||||
{"wglGetSwapIntervalEXT", reinterpret_cast<PROC>(Ext_GetSwapIntervalEXT)},
|
||||
{"wglGetPixelFormatAttribivARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribivARB)},
|
||||
{"wglGetPixelFormatAttribfvARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribfvARB)},
|
||||
{"wglChoosePixelFormatARB", reinterpret_cast<PROC>(Ext_ChoosePixelFormatARB)},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd) {
|
||||
EnsureInitialized();
|
||||
MGLOG_D("wglChoosePixelFormat(hdc=%p)", hdc);
|
||||
// Format 1 (RGBA8 + depth24/stencil8) satisfies every request; a format
|
||||
// exceeding the asked-for capabilities is a legal ChoosePixelFormat answer.
|
||||
(void)pfd;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd) {
|
||||
EnsureInitialized();
|
||||
MGLOG_D("wglDescribePixelFormat(hdc=%p, format=%d)", hdc, format);
|
||||
if (!pfd) {
|
||||
return kPixelFormatCount;
|
||||
}
|
||||
if (size < sizeof(PIXELFORMATDESCRIPTOR) || format < 1 || format > kPixelFormatCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const PixelFormatInfo& info = kPixelFormats[format - 1];
|
||||
std::memset(pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
|
||||
pfd->nSize = sizeof(PIXELFORMATDESCRIPTOR);
|
||||
pfd->nVersion = 1;
|
||||
pfd->dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_SWAP_EXCHANGE
|
||||
#if defined(PFD_SUPPORT_COMPOSITION)
|
||||
| PFD_SUPPORT_COMPOSITION
|
||||
#endif
|
||||
;
|
||||
pfd->iPixelType = PFD_TYPE_RGBA;
|
||||
pfd->cColorBits = 32;
|
||||
pfd->cRedBits = 8;
|
||||
pfd->cRedShift = 16;
|
||||
pfd->cGreenBits = 8;
|
||||
pfd->cGreenShift = 8;
|
||||
pfd->cBlueBits = 8;
|
||||
pfd->cBlueShift = 0;
|
||||
pfd->cAlphaBits = static_cast<BYTE>(info.AlphaBits);
|
||||
pfd->cAlphaShift = 24;
|
||||
pfd->cDepthBits = static_cast<BYTE>(info.DepthBits);
|
||||
pfd->cStencilBits = static_cast<BYTE>(info.StencilBits);
|
||||
pfd->iLayerType = PFD_MAIN_PLANE;
|
||||
return kPixelFormatCount;
|
||||
}
|
||||
|
||||
int GetPixelFormat(HDC hdc) {
|
||||
EnsureInitialized();
|
||||
HWND hwnd = WindowFromDC(hdc);
|
||||
if (!hwnd) {
|
||||
return 0;
|
||||
}
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto& formats = WindowPixelFormats();
|
||||
auto it = formats.find(hwnd);
|
||||
return it == formats.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR*) {
|
||||
EnsureInitialized();
|
||||
MGLOG_D("wglSetPixelFormat(hdc=%p, format=%d)", hdc, format);
|
||||
if (format < 1 || format > kPixelFormatCount) {
|
||||
SetLastError(ERROR_INVALID_PARAMETER);
|
||||
return FALSE;
|
||||
}
|
||||
HWND hwnd = WindowFromDC(hdc);
|
||||
if (!hwnd) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
WindowPixelFormats()[hwnd] = format;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL SwapBuffers(HDC hdc) {
|
||||
HWND hwnd = WindowFromDC(hdc);
|
||||
if (!hwnd) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto& surfaces = WindowSurfaces();
|
||||
auto it = surfaces.find(hwnd);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
SyncSurfaceSize(hwnd, it->second);
|
||||
return EGLImpl::SwapBuffers(it->second.Display, it->second.Surface) == EGL_TRUE ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
HGLRC CreateContext(HDC hdc) {
|
||||
EnsureInitialized();
|
||||
MGLOG_I("wglCreateContext(hdc=%p)", hdc);
|
||||
// A legacy WGL context is a compatibility-profile context; MobileGL keys
|
||||
// its relaxed-semantics mode off the explicit compatibility bit.
|
||||
const EGLint attribs[] = {
|
||||
EGL_CONTEXT_MAJOR_VERSION, 3,
|
||||
EGL_CONTEXT_MINOR_VERSION, 3,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
|
||||
EGL_NONE,
|
||||
};
|
||||
return CreateContextFromEGLAttribs(hdc, nullptr, attribs);
|
||||
}
|
||||
|
||||
BOOL DeleteContext(HGLRC hglrc) {
|
||||
EnsureInitialized();
|
||||
MGLOG_I("wglDeleteContext(%p)", hglrc);
|
||||
if (t_current.Context == hglrc) {
|
||||
MakeCurrent(nullptr, nullptr);
|
||||
}
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(hglrc);
|
||||
if (!object) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
if (object->Context != EGL_NO_CONTEXT) {
|
||||
EGLImpl::DestroyContext(object->Display, object->Context);
|
||||
}
|
||||
Contexts().erase(hglrc);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL MakeCurrent(HDC hdc, HGLRC hglrc) {
|
||||
EnsureInitialized();
|
||||
MGLOG_D("wglMakeCurrent(hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
if (!hglrc) {
|
||||
if (!t_current.Context) {
|
||||
t_current = {};
|
||||
return TRUE;
|
||||
}
|
||||
const EGLBoolean released =
|
||||
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
t_current = {};
|
||||
return released == EGL_TRUE ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
HWND hwnd = WindowFromDC(hdc);
|
||||
if (!hwnd) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
auto* object = TryGetContext(hglrc);
|
||||
if (!object) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
WindowSurface* surface = EnsureWindowSurface(hwnd, *object);
|
||||
if (!surface) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
||||
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
return FALSE;
|
||||
}
|
||||
t_current = {hdc, hglrc};
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
HGLRC GetCurrentContext() {
|
||||
return t_current.Context;
|
||||
}
|
||||
|
||||
HDC GetCurrentDC() {
|
||||
return t_current.DC;
|
||||
}
|
||||
|
||||
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
|
||||
// All MobileGL contexts alias one global GL object namespace, so every
|
||||
// pair of contexts already shares.
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
if (!TryGetContext(hglrcShare) || !TryGetContext(hglrcDest)) {
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
PROC GetProcAddress(const char* name) {
|
||||
EnsureInitialized();
|
||||
if (!name) {
|
||||
return nullptr;
|
||||
}
|
||||
if (name[0] == 'w' && name[1] == 'g' && name[2] == 'l') {
|
||||
for (const auto& entry : kWGLExtensionProcs) {
|
||||
if (std::strcmp(entry.Name, name) == 0) {
|
||||
return entry.Proc;
|
||||
}
|
||||
}
|
||||
MGLOG_D("wglGetProcAddress: unknown wgl entry point %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<PROC>(MG_Impl::GetProcAddress(name));
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::WGLImpl
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -0,0 +1,34 @@
|
||||
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.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(_WIN32)
|
||||
|
||||
namespace MobileGL::MG_Impl::WGLImpl {
|
||||
// Classic opengl32.dll surface. gdi32's ChoosePixelFormat/SetPixelFormat/
|
||||
// DescribePixelFormat/GetPixelFormat/SwapBuffers forward into the loaded
|
||||
// opengl32.dll's wgl* exports, so these back both call paths.
|
||||
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd);
|
||||
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd);
|
||||
int GetPixelFormat(HDC hdc);
|
||||
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR* pfd);
|
||||
BOOL SwapBuffers(HDC hdc);
|
||||
|
||||
HGLRC CreateContext(HDC hdc);
|
||||
BOOL DeleteContext(HGLRC hglrc);
|
||||
BOOL MakeCurrent(HDC hdc, HGLRC hglrc);
|
||||
HGLRC GetCurrentContext();
|
||||
HDC GetCurrentDC();
|
||||
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest);
|
||||
|
||||
PROC GetProcAddress(const char* name);
|
||||
} // namespace MobileGL::MG_Impl::WGLImpl
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -368,6 +368,26 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool EGLContext::HasAnyInitializedDisplay() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for (const auto& [handle, displayObject] : m_displays) {
|
||||
if (displayObject.Initialized) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool EGLContext::HasAnyCurrentContext() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for (const auto& [threadId, current] : m_threadCurrents) {
|
||||
if (current.Context != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
||||
EGLint configSize, EGLint* numConfig) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
@@ -764,6 +784,20 @@ namespace MobileGL {
|
||||
ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1);
|
||||
}
|
||||
|
||||
Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||
if (currentIt == m_threadCurrents.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* ctx = TryGetContext(currentIt->second.Context);
|
||||
// Affirmative check: true only when the host explicitly requested the
|
||||
// compatibility bit. Attrib-less contexts report false and thus read as core
|
||||
// for GL_CONTEXT_PROFILE_MASK (EGL defaults 3.x contexts to the core profile).
|
||||
return ctx && ctx->ClientAPI == EGL_OPENGL_API &&
|
||||
(ctx->OpenGLProfileMask & EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT);
|
||||
}
|
||||
|
||||
EGLint EGLContext::GetCurrentContextFlags() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||
@@ -1401,6 +1435,7 @@ namespace MobileGL {
|
||||
}
|
||||
} // namespace EGLState
|
||||
|
||||
UniquePtr<EGLState::EGLContext> pEGLContext;
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -39,6 +39,10 @@ namespace MobileGL {
|
||||
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
|
||||
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
|
||||
Bool TerminateDisplay(EGLDisplayHandle display);
|
||||
// Whole-library idle checks used by EGLImpl::Terminate to decide
|
||||
// when the last eglTerminate may tear MobileGL down entirely.
|
||||
Bool HasAnyInitializedDisplay() const;
|
||||
Bool HasAnyCurrentContext() const;
|
||||
|
||||
// Config
|
||||
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
||||
@@ -59,6 +63,7 @@ namespace MobileGL {
|
||||
Bool ValidateContext(EGLContextHandle context) const;
|
||||
Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const;
|
||||
Bool IsCurrentContextOpenGLCoreProfile() const;
|
||||
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
|
||||
EGLint GetCurrentContextFlags() const;
|
||||
|
||||
// Surface
|
||||
@@ -261,6 +266,6 @@ namespace MobileGL {
|
||||
};
|
||||
} // namespace EGLState
|
||||
|
||||
extern UniquePtr<EGLState::EGLContext> pEGLContext;
|
||||
extern UniquePtr<EGLState::EGLContext>& pEGLContext;
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -231,6 +231,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_resource.Bytes();
|
||||
}
|
||||
|
||||
Bool BufferObject::EnsureGpuResidentStorage() {
|
||||
if (m_resource.IsGpuResident()) {
|
||||
return true;
|
||||
}
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||
return false;
|
||||
}
|
||||
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
|
||||
if (base == nullptr) {
|
||||
return false;
|
||||
}
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
return true;
|
||||
}
|
||||
|
||||
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
|
||||
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
||||
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
||||
|
||||
@@ -133,6 +133,11 @@ namespace MobileGL {
|
||||
|
||||
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
|
||||
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
|
||||
// Adopt backend host-visible coherent GPU storage as the source of truth
|
||||
// (used for GPU-written targets like transform feedback capture, so
|
||||
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
|
||||
// resident or when the backend declines.
|
||||
Bool EnsureGpuResidentStorage();
|
||||
void ReleaseMemory();
|
||||
void FlushMemoryRange(SizeT offset, SizeT length);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Core.h"
|
||||
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
|
||||
#include "MG_State/EGLState/Core.h"
|
||||
#include <Config.h>
|
||||
|
||||
namespace MobileGL::MG_State {
|
||||
void Init() {
|
||||
@@ -17,6 +18,11 @@ namespace MobileGL::MG_State {
|
||||
pEGLContext = MakeUnique<EGLState::EGLContext>();
|
||||
}
|
||||
|
||||
Bool IsRelaxedSemanticsActive() {
|
||||
return MG_Config::Features.RelaxedSemantics ||
|
||||
!(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
|
||||
}
|
||||
|
||||
namespace GLState {
|
||||
// Error
|
||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
@@ -226,12 +232,16 @@ namespace MobileGL::MG_State {
|
||||
return m_textureState.GetTextureObject(index);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
|
||||
return m_textureState.GetDefaultTextureObject(target);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
|
||||
return m_textureState.CreateTextureObject(index, target);
|
||||
}
|
||||
|
||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||
m_textureState.MarkTextureObjectForDeletion(index);
|
||||
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
||||
}
|
||||
|
||||
TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
|
||||
@@ -279,6 +289,10 @@ namespace MobileGL::MG_State {
|
||||
return m_programState.MarkShaderObjectForDeletion(index);
|
||||
}
|
||||
|
||||
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
|
||||
return m_programState.ReleaseShaderNameIfOrphaned(index);
|
||||
}
|
||||
|
||||
Bool GLContext::ValidateProgramName(const Uint index) const {
|
||||
return m_programState.ValidateProgramObject(index);
|
||||
}
|
||||
@@ -707,5 +721,6 @@ namespace MobileGL::MG_State {
|
||||
}
|
||||
} // namespace GLState
|
||||
|
||||
UniquePtr<GLState::GLContext> pGLContext;
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
|
||||
} // namespace MobileGL::MG_State
|
||||
|
||||
@@ -95,6 +95,8 @@ namespace MobileGL {
|
||||
// Texture
|
||||
void GenTextureNames(Uint number, Vector<Uint>& textures);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
@@ -106,6 +108,7 @@ namespace MobileGL {
|
||||
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
|
||||
// per-draw sampled-texture set.
|
||||
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
|
||||
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
|
||||
Bool ValidateTextureName(Uint index) const;
|
||||
Bool ValidateTextureObject(Uint index) const;
|
||||
Int GetActiveTextureUnit() const;
|
||||
@@ -116,6 +119,9 @@ namespace MobileGL {
|
||||
Uint CreateShader(ShaderStage stage);
|
||||
void MarkProgramForDeletion(Uint index);
|
||||
void MarkShaderForDeletion(Uint index);
|
||||
// Frees a deletion-flagged shader's name once it lost its last GL-visible
|
||||
// attachment (call after glDetachShader).
|
||||
void ReleaseShaderNameIfOrphaned(Uint index);
|
||||
Bool ValidateProgramName(Uint index) const;
|
||||
Bool ValidateShaderName(Uint index) const;
|
||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
||||
@@ -205,6 +211,47 @@ namespace MobileGL {
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
|
||||
// Transform feedback (GL 3.0 core Begin/End; no feedback objects yet)
|
||||
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
|
||||
m_transformFeedbackActive = true;
|
||||
m_transformFeedbackPrimitiveMode = primitiveMode;
|
||||
m_transformFeedbackProgram = program;
|
||||
++m_transformFeedbackGeneration;
|
||||
m_transformFeedbackCapturedVertices = 0;
|
||||
m_transformFeedbackInputPrimitives = 0;
|
||||
}
|
||||
void EndTransformFeedback() {
|
||||
m_transformFeedbackActive = false;
|
||||
m_transformFeedbackProgram.reset();
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
|
||||
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
return m_transformFeedbackProgram;
|
||||
}
|
||||
// Bumped on every BeginTransformFeedback; the backend uses it to
|
||||
// distinguish "resume appending" from "fresh capture".
|
||||
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
|
||||
// CPU-side primitive accounting for the transform feedback queries:
|
||||
// every captured draw adds its primitive count (draws without a
|
||||
// geometry stage write exactly what they generate).
|
||||
void AddTransformFeedbackPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackPrimitiveCounter += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
|
||||
// Vertices already captured since BeginTransformFeedback (drives the
|
||||
// buffer-capacity clamp on the primitives-written accounting).
|
||||
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
|
||||
m_transformFeedbackCapturedVertices += vertices;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
|
||||
// Raw assembled input primitives fed to the capture stage since Begin
|
||||
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
|
||||
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackInputPrimitives += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
|
||||
|
||||
// Framebuffer
|
||||
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||
@@ -237,6 +284,13 @@ namespace MobileGL {
|
||||
BufferState m_bufferState;
|
||||
VertexArrayState m_vertexArrayState;
|
||||
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
Uint64 m_transformFeedbackGeneration = 0;
|
||||
Uint64 m_transformFeedbackPrimitiveCounter = 0;
|
||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||
Uint64 m_transformFeedbackInputPrimitives = 0;
|
||||
TextureState m_textureState;
|
||||
ProgramState m_programState;
|
||||
RenderState m_renderState;
|
||||
@@ -246,6 +300,13 @@ namespace MobileGL {
|
||||
};
|
||||
} // namespace GLState
|
||||
|
||||
extern UniquePtr<GLState::GLContext> pGLContext;
|
||||
extern UniquePtr<GLState::GLContext>& pGLContext;
|
||||
|
||||
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
||||
// current EGL context explicitly requested a core profile (core bit in
|
||||
// EGL_CONTEXT_OPENGL_PROFILE_MASK, or a >=3.1 version request without the compatibility
|
||||
// bit) and MOBILEGL_RELAXED_SEMANTICS is off; no current context, legacy version
|
||||
// requests, and the compatibility bit all relax.
|
||||
Bool IsRelaxedSemanticsActive();
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Error.h"
|
||||
#include <algorithm>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
|
||||
@@ -19,7 +20,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGLOG_E("Recording OpenGL error (%s):\n%s",
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
|
||||
info->toString().c_str());
|
||||
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
|
||||
// error flags, each is set only while currently unset — repeated errors of the same
|
||||
// code are discarded until glGetError reads the flag. Unbounded accumulation leaked
|
||||
// stale errors into later, unrelated glGetError checks (GL CTS deinit noise).
|
||||
const Bool alreadyPending = std::any_of(m_errors.begin(), m_errors.end(),
|
||||
[code](const auto& e) { return e->code == code; });
|
||||
if (!alreadyPending) {
|
||||
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user