mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
Compare commits
156
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4322427e78 | ||
|
|
203d4bce5e | ||
|
|
761114d022 | ||
|
|
9caf34d5b1 | ||
|
|
5e676b338b | ||
|
|
db01bfa3e8 | ||
|
|
cc3dcfd80e | ||
|
|
c8632dfefe | ||
|
|
d9556ff041 | ||
|
|
b8a8a660e1 | ||
|
|
39f21e52ea | ||
|
|
0e933b8f2f | ||
|
|
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
|
||||
|
||||
+44
-2
@@ -188,8 +188,15 @@ 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/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -299,6 +306,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 +341,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 +395,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}
|
||||
@@ -475,6 +509,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 +527,6 @@ if (NOT ANDROID)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_TRACE_REPLAY)
|
||||
enable_testing()
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -20,6 +20,15 @@ namespace MobileGL::MG_Config {
|
||||
|
||||
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,32 @@ 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_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||
QuirkOverride ClipDistanceQuirk = 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,12 @@ 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.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||
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
|
||||
|
||||
+38
-33
@@ -9,14 +9,24 @@
|
||||
#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 <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) {
|
||||
@@ -64,40 +74,35 @@ 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 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.
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
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
|
||||
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
|
||||
// so a fresh init can follow a full Destroy() (e.g. after the last
|
||||
// eglTerminate).
|
||||
void EnsureInitialized();
|
||||
void Destroy();
|
||||
|
||||
namespace MG_Util::Debug {
|
||||
|
||||
@@ -230,8 +230,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 +287,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;
|
||||
@@ -285,13 +306,15 @@ namespace MobileGL {
|
||||
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
|
||||
|
||||
@@ -605,9 +605,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL 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 +627,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 +673,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 +701,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 +820,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 +838,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;
|
||||
}
|
||||
|
||||
@@ -938,8 +948,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 +1010,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;
|
||||
@@ -1007,6 +1036,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
||||
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);
|
||||
|
||||
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/
|
||||
@@ -242,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
Uint m_backendVAOId = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
// Attribs the frontend has Enabled but that have no source at all (no buffer object
|
||||
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
|
||||
// driver treats them as client arrays and memcpys from address 0 at draw time
|
||||
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
|
||||
Uint32 m_forceDisabledAttribsMask = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||
@@ -254,10 +275,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 +352,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 +369,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 +396,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 +407,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 +444,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 {
|
||||
@@ -338,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
|
||||
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
|
||||
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
|
||||
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
|
||||
static const std::regex redeclarationRegex(
|
||||
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
Bool firstLine = true;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
if (!std::regex_match(line, redeclarationRegex)) {
|
||||
if (!firstLine) {
|
||||
result += '\n';
|
||||
}
|
||||
result += line;
|
||||
firstLine = false;
|
||||
}
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
@@ -448,4 +487,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);
|
||||
@@ -52,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -140,6 +140,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 +411,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;
|
||||
}
|
||||
|
||||
@@ -488,14 +503,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,7 +519,7 @@ 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_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};
|
||||
@@ -516,6 +532,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;
|
||||
}
|
||||
|
||||
@@ -630,7 +653,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 +704,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 +761,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);
|
||||
@@ -765,5 +809,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
switch (topology) {
|
||||
@@ -108,6 +109,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 +228,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,
|
||||
@@ -258,6 +336,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,6 +65,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||
void DestroyAll();
|
||||
|
||||
// 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:
|
||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||
|
||||
@@ -70,5 +94,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return targetEnv;
|
||||
}
|
||||
|
||||
// Cheap raw-word scan for an `OpDecorate <id> BuiltIn InstanceIndex` decoration. Used
|
||||
// only to decide whether to warn when shaderDrawParameters is unavailable; a false
|
||||
// negative merely suppresses a diagnostic.
|
||||
Bool SpirvDeclaresInstanceIndexBuiltin(const Vector<Uint>& spirv) {
|
||||
constexpr Uint32 kSpirvMagicNumber = 0x07230203u;
|
||||
constexpr SizeT kHeaderWordCount = 5;
|
||||
if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SizeT wordIndex = kHeaderWordCount;
|
||||
while (wordIndex < spirv.size()) {
|
||||
const Uint32 firstWord = spirv[wordIndex];
|
||||
const Uint32 wordCount = firstWord >> 16;
|
||||
const auto opcode = static_cast<spv::Op>(firstWord & 0xffffu);
|
||||
if (wordCount == 0 || wordIndex + wordCount > spirv.size()) {
|
||||
break;
|
||||
}
|
||||
if (opcode == spv::Op::OpDecorate && wordCount >= 4 &&
|
||||
static_cast<spv::Decoration>(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) {
|
||||
return true;
|
||||
}
|
||||
wordIndex += wordCount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
|
||||
if (spirv.empty() || spirvId == 0) {
|
||||
return false;
|
||||
@@ -1122,9 +1094,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto* binding : bindings) {
|
||||
MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record");
|
||||
const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type);
|
||||
MOBILEGL_ASSERT(binding->count == 1,
|
||||
"ProgramFactory: descriptor arrays are unsupported (name='%s' count=%u)",
|
||||
binding->name ? binding->name : "<null>", binding->count);
|
||||
// UBO instance arrays (uniform Block {...} b[N];) occupy one binding with
|
||||
// descriptorCount = N; other descriptor arrays stay unsupported and must
|
||||
// fail program creation cleanly rather than continue with corrupt state.
|
||||
if (binding->count != 1 && kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
|
||||
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||
"kind (name='%s' count=%u type=%d)",
|
||||
binding->name ? binding->name : "<null>", binding->count,
|
||||
static_cast<Int>(binding->descriptor_type));
|
||||
destroyReflectModules();
|
||||
return false;
|
||||
}
|
||||
|
||||
DescriptorKey key{};
|
||||
key.kind = kind;
|
||||
@@ -1197,8 +1177,62 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
|
||||
switch (uniformType) {
|
||||
case GL_IMAGE_1D:
|
||||
case GL_IMAGE_2D:
|
||||
case GL_IMAGE_3D:
|
||||
case GL_IMAGE_2D_RECT:
|
||||
case GL_IMAGE_CUBE:
|
||||
case GL_IMAGE_BUFFER:
|
||||
case GL_IMAGE_1D_ARRAY:
|
||||
case GL_IMAGE_2D_ARRAY:
|
||||
case GL_IMAGE_CUBE_MAP_ARRAY:
|
||||
case GL_IMAGE_2D_MULTISAMPLE:
|
||||
case GL_IMAGE_2D_MULTISAMPLE_ARRAY:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
|
||||
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
|
||||
// position math between pipelines cannot desynchronize it; the blended depth-write
|
||||
// quirk therefore leaves it alone (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool ProgramFactory::ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 modeIndex = 0; modeIndex < entryPoint.execution_mode_count; ++modeIndex) {
|
||||
if (entryPoint.execution_modes[modeIndex] == SpvExecutionModeDepthReplacing) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// glslang's relaxed-Vulkan mode maps GL's gl_InstanceID onto the InstanceIndex builtin.
|
||||
// Without shaderDrawParameters there is no gl_BaseInstance to subtract, so such a shader
|
||||
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
|
||||
// detects the case so the user gets one warning instead of silent corruption.
|
||||
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == SpvBuiltInInstanceIndex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
||||
switch (stage) {
|
||||
case ShaderStage::Vertex:
|
||||
@@ -1218,6 +1252,105 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) {
|
||||
switch (format) {
|
||||
case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED;
|
||||
case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT;
|
||||
case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT;
|
||||
case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT;
|
||||
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||
case SpvImageFormatR8: return VK_FORMAT_R8_UNORM;
|
||||
case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM;
|
||||
case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM;
|
||||
case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM;
|
||||
case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM;
|
||||
case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case SpvImageFormatR32i: return VK_FORMAT_R32_SINT;
|
||||
case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT;
|
||||
case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT;
|
||||
case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT;
|
||||
case SpvImageFormatR16i: return VK_FORMAT_R16_SINT;
|
||||
case SpvImageFormatR8i: return VK_FORMAT_R8_SINT;
|
||||
case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT;
|
||||
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||
case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT;
|
||||
case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT;
|
||||
case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT;
|
||||
case SpvImageFormatR64i: return VK_FORMAT_R64_SINT;
|
||||
case SpvImageFormatMax: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) {
|
||||
switch (glType) {
|
||||
case GL_INT_SAMPLER_1D:
|
||||
case GL_INT_SAMPLER_2D:
|
||||
case GL_INT_SAMPLER_3D:
|
||||
case GL_INT_SAMPLER_CUBE:
|
||||
case GL_INT_SAMPLER_2D_RECT:
|
||||
case GL_INT_SAMPLER_1D_ARRAY:
|
||||
case GL_INT_SAMPLER_2D_ARRAY:
|
||||
case GL_INT_SAMPLER_BUFFER:
|
||||
case GL_INT_SAMPLER_2D_MULTISAMPLE:
|
||||
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||
return SamplerNumericDomain::SignedInteger;
|
||||
case GL_UNSIGNED_INT_SAMPLER_1D:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D:
|
||||
case GL_UNSIGNED_INT_SAMPLER_3D:
|
||||
case GL_UNSIGNED_INT_SAMPLER_CUBE:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
|
||||
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
|
||||
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||
return SamplerNumericDomain::UnsignedInteger;
|
||||
case GL_SAMPLER_1D:
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_3D:
|
||||
case GL_SAMPLER_CUBE:
|
||||
case GL_SAMPLER_2D_RECT:
|
||||
case GL_SAMPLER_1D_ARRAY:
|
||||
case GL_SAMPLER_2D_ARRAY:
|
||||
case GL_SAMPLER_BUFFER:
|
||||
case GL_SAMPLER_2D_MULTISAMPLE:
|
||||
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY:
|
||||
case GL_SAMPLER_1D_SHADOW:
|
||||
case GL_SAMPLER_2D_SHADOW:
|
||||
case GL_SAMPLER_CUBE_SHADOW:
|
||||
case GL_SAMPLER_2D_RECT_SHADOW:
|
||||
case GL_SAMPLER_1D_ARRAY_SHADOW:
|
||||
case GL_SAMPLER_2D_ARRAY_SHADOW:
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
|
||||
return SamplerNumericDomain::Float;
|
||||
default:
|
||||
return SamplerNumericDomain::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
@@ -1347,6 +1480,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||
if (!s_warnedInstanceIndexUnsupported) {
|
||||
s_warnedInstanceIndexUnsupported = true;
|
||||
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
|
||||
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t inputCount = 0;
|
||||
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
||||
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
||||
@@ -1394,6 +1536,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeFragmentOutputLocationMask = 0;
|
||||
entry.fragmentOutputTypes.fill(0);
|
||||
entry.fragmentReplacesDepth = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
||||
@@ -1412,9 +1555,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
|
||||
static_cast<Int>(createResult));
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the exemption: stripping a genuine gl_FragDepth writer would
|
||||
// corrupt its depth output outright, while wrongly exempting an accumulation
|
||||
// pass merely reverts that one program to the pre-quirk behavior.
|
||||
entry.fragmentReplacesDepth = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule);
|
||||
|
||||
uint32_t outputCount = 0;
|
||||
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
|
||||
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
||||
@@ -1465,10 +1614,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.samplerNameByBinding.assign(m_maxBindings, String());
|
||||
entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
|
||||
entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
|
||||
entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown);
|
||||
entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED);
|
||||
entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false);
|
||||
entry.storageBlockNameByBinding.assign(m_maxBindings, String());
|
||||
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
|
||||
entry.globalUboBinding = -1;
|
||||
entry.dynamicBindings.clear();
|
||||
entry.bindingDescriptorCounts.assign(m_maxBindings, 1);
|
||||
entry.arrayedUniformBlockIndicesByBinding.clear();
|
||||
|
||||
// Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module
|
||||
for (const auto& module : spirv) {
|
||||
@@ -1484,6 +1638,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)",
|
||||
static_cast<Int>(createReflectResult));
|
||||
|
||||
// Descriptor counts per binding (UBO instance arrays reflect count > 1).
|
||||
UnorderedMap<Uint32, Uint32> descriptorCountByBinding;
|
||||
{
|
||||
uint32_t countProbe = 0;
|
||||
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS &&
|
||||
countProbe > 0) {
|
||||
Vector<SpvReflectDescriptorBinding*> probeBindings(countProbe);
|
||||
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe,
|
||||
probeBindings.data()) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS) {
|
||||
for (const auto* probeBinding : probeBindings) {
|
||||
if (probeBinding != nullptr) {
|
||||
descriptorCountByBinding[probeBinding->binding] =
|
||||
std::max<Uint32>(1, probeBinding->count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect uniform buffers
|
||||
auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER);
|
||||
for (const auto& ubo : ubos) {
|
||||
@@ -1509,9 +1684,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
||||
if (blockIndex == 0xFFFFFFFFu) {
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
|
||||
const auto countIt = descriptorCountByBinding.find(binding);
|
||||
const Uint32 descriptorCount =
|
||||
countIt != descriptorCountByBinding.end() ? countIt->second : 1u;
|
||||
|
||||
if (descriptorCount <= 1) {
|
||||
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
||||
if (blockIndex == 0xFFFFFFFFu) {
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
|
||||
ubo.name.c_str(), binding);
|
||||
continue;
|
||||
}
|
||||
|
||||
MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None ||
|
||||
entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic,
|
||||
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
|
||||
binding, ubo.name.c_str());
|
||||
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
|
||||
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
|
||||
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
|
||||
ubo.name.c_str(), binding);
|
||||
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
|
||||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
|
||||
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
|
||||
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
|
||||
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// UBO instance array: one binding, descriptorCount elements. GL exposes each
|
||||
// element as its own active block named "Name[i]"; map every element to its
|
||||
// GL block index so the descriptor write can gather per-element buffer ranges.
|
||||
if (descriptorCount > m_maxBindings) {
|
||||
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; "
|
||||
"leaving binding %u unmapped",
|
||||
ubo.name.c_str(), descriptorCount, m_maxBindings, binding);
|
||||
continue;
|
||||
}
|
||||
Vector<Int> elementBlockIndices;
|
||||
elementBlockIndices.reserve(descriptorCount);
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
String elementName = ubo.name + "[" + std::to_string(element) + "]";
|
||||
Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str());
|
||||
if (elementBlockIndex == 0xFFFFFFFFu && element == 0) {
|
||||
// Some frontends report the first element under the bare block name.
|
||||
elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
||||
}
|
||||
if (elementBlockIndex == 0xFFFFFFFFu) {
|
||||
// Degrade rather than corrupt: reuse element 0's block if we have one,
|
||||
// otherwise give up on the binding (same observable behavior as an
|
||||
// inactive block: wrong values, but no crash).
|
||||
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active "
|
||||
"GL uniform block",
|
||||
ubo.name.c_str(), element);
|
||||
if (!elementBlockIndices.empty()) {
|
||||
elementBlockIndex = static_cast<Uint>(elementBlockIndices.front());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
elementBlockIndices.push_back(static_cast<Int>(elementBlockIndex));
|
||||
}
|
||||
if (elementBlockIndices.size() != descriptorCount) {
|
||||
MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u",
|
||||
ubo.name.c_str(), binding);
|
||||
continue;
|
||||
}
|
||||
@@ -1521,14 +1756,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
|
||||
binding, ubo.name.c_str());
|
||||
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
|
||||
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
|
||||
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
|
||||
ubo.name.c_str(), binding);
|
||||
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
|
||||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
|
||||
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
|
||||
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
|
||||
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
||||
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(descriptorCount);
|
||||
entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0];
|
||||
entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices);
|
||||
}
|
||||
|
||||
// Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs.
|
||||
@@ -1592,10 +1822,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const TextureTarget target = UniformTypeToTextureTarget(program.GetUniformType(static_cast<Uint>(location)));
|
||||
const GLenum uniformType = program.GetUniformType(static_cast<Uint>(location));
|
||||
|
||||
if (descriptorKind == DescriptorBindingKind::StorageImage) {
|
||||
const VkFormat reflectedFormat =
|
||||
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
|
||||
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
|
||||
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
|
||||
reflectedFormat == VK_FORMAT_UNDEFINED ||
|
||||
existingFormat == reflectedFormat,
|
||||
"ProgramFactory::ReflectLayout: storage image binding %u ('%s') has "
|
||||
"conflicting reflected formats (%d vs %d)",
|
||||
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
|
||||
static_cast<Int>(reflectedFormat));
|
||||
if (existingFormat == VK_FORMAT_UNDEFINED) {
|
||||
existingFormat = reflectedFormat;
|
||||
}
|
||||
if (m_unformattedFloatStorageImagesEnabled &&
|
||||
existingFormat == VK_FORMAT_UNDEFINED &&
|
||||
IsFloatStorageImageUniformType(uniformType)) {
|
||||
entry.storageImageUsesBindingFormatByBinding[binding] = true;
|
||||
} else if (reflectedFormat != VK_FORMAT_UNDEFINED) {
|
||||
// A typed declaration in any stage wins for the entire binding. This is
|
||||
// required when another stage reaches the same image through an atomic
|
||||
// path and therefore could not be made formatless.
|
||||
entry.storageImageUsesBindingFormatByBinding[binding] = false;
|
||||
}
|
||||
}
|
||||
|
||||
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
|
||||
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
||||
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
||||
uniformName.c_str());
|
||||
if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) {
|
||||
const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType);
|
||||
MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown,
|
||||
"ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain "
|
||||
"for '%s' (uniformType=0x%x)",
|
||||
uniformName.c_str(), uniformType);
|
||||
MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] ==
|
||||
SamplerNumericDomain::Unknown ||
|
||||
entry.samplerNumericDomainByBinding[binding] == numericDomain,
|
||||
"ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting "
|
||||
"numeric domains (%d vs %d)",
|
||||
binding, uniformName.c_str(),
|
||||
static_cast<Int>(entry.samplerNumericDomainByBinding[binding]),
|
||||
static_cast<Int>(numericDomain));
|
||||
entry.samplerNumericDomainByBinding[binding] = numericDomain;
|
||||
}
|
||||
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
|
||||
entry.samplerUniformLocationByBinding[binding] == location,
|
||||
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
|
||||
@@ -1631,7 +1905,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkDescriptorSetLayoutBinding layoutBinding{};
|
||||
layoutBinding.binding = binding;
|
||||
layoutBinding.descriptorCount = 1;
|
||||
layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding];
|
||||
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL;
|
||||
layoutBinding.pImmutableSamplers = nullptr;
|
||||
if (kind == DescriptorBindingKind::UniformBufferDynamic) {
|
||||
@@ -1643,6 +1917,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
} else if (kind == DescriptorBindingKind::StorageImage) {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||
entry.hasStorageImages = true;
|
||||
} else {
|
||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
}
|
||||
@@ -1697,28 +1972,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||
// per-pipeline compilers cannot vary the position math between passes.
|
||||
{
|
||||
Vector<Uint> invariantSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||
moduleSpirvs[i], invariantSpirv)) {
|
||||
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||
} else {
|
||||
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
||||
// fails open and keeps the undecorated words - which silently reinstates
|
||||
// the multi-pass invariance bug rather than breaking anything loudly.
|
||||
MGLOG_E("ProgramFactory: position-invariant decoration failed for program %u; "
|
||||
"keeping the original module - multi-pass depth-equality chains "
|
||||
"(e.g. MC 26.3 OIT clouds) may drop primitives on this device",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
|
||||
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
|
||||
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
|
||||
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
if (m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||
"instanced draws with a non-zero baseInstance may render incorrectly",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
} else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) {
|
||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||
if (!s_warnedInstanceIndexUnsupported) {
|
||||
s_warnedInstanceIndexUnsupported = true;
|
||||
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
|
||||
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
|
||||
}
|
||||
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
||||
// the builtin but shaderDrawParameters is missing) rides along with
|
||||
// ReflectVertexInputs, which already reflects this stage.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||
m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||
"instanced draws with a non-zero baseInstance may render incorrectly",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// When Vulkan can legally access storage images without a statically declared
|
||||
// format, let GL's glBindImageTexture format select the runtime image view. This
|
||||
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
|
||||
// float image qualifier can disagree with the bound render-target format. Integer
|
||||
// storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the
|
||||
// exact descriptor format required by their shader operations.
|
||||
if (m_unformattedFloatStorageImagesEnabled) {
|
||||
Vector<Uint> unformattedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
moduleSpirvs[i], unformattedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(unformattedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -51,11 +59,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 +84,10 @@ 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;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
|
||||
@@ -79,11 +103,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 +123,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
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;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
if (this == &other) {
|
||||
@@ -115,11 +149,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 +169,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
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;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -167,9 +211,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -180,6 +226,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
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,6 +262,9 @@ 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;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#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"
|
||||
@@ -78,6 +79,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,
|
||||
@@ -276,6 +287,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
|
||||
@@ -291,23 +351,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) {
|
||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
||||
memo.forceNearestFiltering == forceNearestFiltering) {
|
||||
resolvedSampler = memo.sampler;
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
memo.samplerLifetimeId = samplerLifetimeId;
|
||||
memo.samplerVersion = samplerVersion;
|
||||
memo.textureLifetimeId = textureLifetimeId;
|
||||
memo.textureParamsVersion = textureParamsVersion;
|
||||
memo.forceNearestFiltering = forceNearestFiltering;
|
||||
memo.sampler = resolvedSampler;
|
||||
memo.valid = true;
|
||||
}
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
}
|
||||
outImageInfo = {
|
||||
.sampler = resolvedSampler,
|
||||
.imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
|
||||
.imageView = sampledImageView,
|
||||
.imageLayout = resource->layout,
|
||||
};
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
@@ -360,6 +424,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 +450,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 +636,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 +700,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 +719,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 +791,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);
|
||||
|
||||
@@ -885,11 +1050,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 +1078,51 @@ 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 {
|
||||
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);
|
||||
}
|
||||
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) ||
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 +52,12 @@ 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);
|
||||
|
||||
private:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
@@ -107,7 +116,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(
|
||||
@@ -165,9 +174,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
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
|
||||
|
||||
|
||||
@@ -66,6 +66,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 +75,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 +85,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 +121,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,6 +157,7 @@ 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);
|
||||
}
|
||||
@@ -117,6 +172,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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();
|
||||
@@ -282,4 +338,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,6 +19,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
enum class VertexStreamConversion : Uint8 {
|
||||
None = 0,
|
||||
Repack,
|
||||
ScaledIntegerToFloat32,
|
||||
};
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
@@ -27,6 +33,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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.
|
||||
@@ -36,8 +43,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -56,8 +63,12 @@ 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;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -197,16 +197,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) {
|
||||
|
||||
@@ -124,7 +124,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; }
|
||||
|
||||
@@ -251,11 +251,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
const VkFormat format = 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;
|
||||
|
||||
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||
const Bool needsCreate =
|
||||
@@ -285,7 +287,7 @@ 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;
|
||||
|
||||
@@ -386,6 +388,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},
|
||||
@@ -602,6 +616,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -623,12 +638,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
||||
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 +696,83 @@ 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;
|
||||
rbDesc.flags = 0;
|
||||
rbDesc.format = rbResource->format;
|
||||
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(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 +791,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 +817,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (hasClear) {
|
||||
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
||||
.attachmentIndex = attachmentIndex,
|
||||
.colorAttachmentSlot = i,
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
@@ -974,6 +1074,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 +1129,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 +1161,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 +1171,36 @@ 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
it = m_renderPasses.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||
// TODO: Transition all the attachments into proper layout before starting the render pass
|
||||
VkRenderPassBeginInfo renderPassBeginInfo;
|
||||
|
||||
@@ -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;
|
||||
@@ -68,7 +73,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 +92,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 +106,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 +118,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hasDepthStencilAttachment(hasDepthStencilAttachment),
|
||||
sampleCount(sampleCount),
|
||||
extent(extent),
|
||||
subpass(subpass)
|
||||
layers(layers)
|
||||
{}
|
||||
|
||||
~RenderPassEntry() {
|
||||
@@ -166,6 +175,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,6 +190,8 @@ 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;
|
||||
|
||||
// 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
|
||||
@@ -197,6 +211,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
@@ -213,6 +228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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{};
|
||||
@@ -221,10 +244,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
|
||||
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();
|
||||
|
||||
@@ -58,11 +58,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) {
|
||||
@@ -77,10 +92,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering) 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)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
const auto magFilter = sampler.GetMagFilter();
|
||||
@@ -99,6 +117,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,8 +132,9 @@ 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) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
return it->second.handle;
|
||||
@@ -118,15 +142,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
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 ? 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);
|
||||
|
||||
@@ -24,13 +24,18 @@ 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();
|
||||
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false);
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
@@ -40,7 +45,8 @@ private:
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
@@ -49,9 +55,17 @@ 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;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
||||
@@ -63,6 +66,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
|
||||
}
|
||||
|
||||
static Bool IsMutableStorageImageFormat(VkFormat format) {
|
||||
if (!vkuFormatIsColor(format) || vkuFormatIsCompressed(format)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// These are the uncompressed color compatibility classes covered by the core GLSL/SPIR-V
|
||||
// storage-image formats. OpenGL mutable texture storage uses image-format compatibility by
|
||||
// size, so a shader may legally reinterpret (for example) RGBA16_UNORM storage as rgba16f. Vulkan
|
||||
// requires the image to be mutable and the view formats to share this exact compatibility
|
||||
// class for the equivalent operation.
|
||||
switch (vkuFormatCompatibilityClass(format)) {
|
||||
case VKU_FORMAT_COMPATIBILITY_CLASS_8BIT:
|
||||
case VKU_FORMAT_COMPATIBILITY_CLASS_16BIT:
|
||||
case VKU_FORMAT_COMPATIBILITY_CLASS_32BIT:
|
||||
case VKU_FORMAT_COMPATIBILITY_CLASS_64BIT:
|
||||
case VKU_FORMAT_COMPATIBILITY_CLASS_128BIT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Bool HasMatchingColorComponentLayout(VkFormat lhs, VkFormat rhs) {
|
||||
const VKU_FORMAT_INFO lhsInfo = vkuGetFormatInfo(lhs);
|
||||
const VKU_FORMAT_INFO rhsInfo = vkuGetFormatInfo(rhs);
|
||||
if (lhsInfo.component_count == 0 || lhsInfo.component_count != rhsInfo.component_count ||
|
||||
lhsInfo.texel_block_size != rhsInfo.texel_block_size ||
|
||||
lhsInfo.texels_per_block != 1 || rhsInfo.texels_per_block != 1) {
|
||||
return false;
|
||||
}
|
||||
for (Uint32 component = 0; component < lhsInfo.component_count; ++component) {
|
||||
if (lhsInfo.components[component].type != rhsInfo.components[component].type ||
|
||||
lhsInfo.components[component].size != rhsInfo.components[component].size) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool FormatMatchesSamplerNumericDomain(VkFormat format, SamplerNumericDomain numericDomain) {
|
||||
switch (numericDomain) {
|
||||
case SamplerNumericDomain::Float:
|
||||
return vkuFormatIsSampledFloat(format);
|
||||
case SamplerNumericDomain::SignedInteger:
|
||||
return vkuFormatIsSINT(format);
|
||||
case SamplerNumericDomain::UnsignedInteger:
|
||||
return vkuFormatIsUINT(format);
|
||||
case SamplerNumericDomain::Unknown:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
switch (requestedSamples) {
|
||||
case 1:
|
||||
@@ -319,7 +375,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
// Legacy low-bit RGB formats share the UNorm8 canonical shadow layout (see
|
||||
// TextureFormatProcessor), so they upload exactly like RGB8 with an alpha expand.
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
||||
// Low-bit RGBA formats: UNorm8x4 canonical shadow, no expansion needed.
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
return {VK_FORMAT_R8G8B8A8_UNORM, false, 0, {0, 0, 0, 0}};
|
||||
// 10/12-bit RGB(A): UNorm16 canonical shadow.
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}};
|
||||
case TextureInternalFormat::RGBA12:
|
||||
return {VK_FORMAT_R16G16B16A16_UNORM, false, 0, {0, 0, 0, 0}};
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
@@ -767,6 +839,180 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return perMipSampledView;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||
VkFormat format) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
|
||||
resource->sampledView == VK_NULL_HANDLE) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
|
||||
return resource->sampledView;
|
||||
}
|
||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const TextureResource::SampledImageViewKey key{
|
||||
.baseMipLevel = resource->sampledBaseMipLevel,
|
||||
.levelCount = resource->sampledLevelCount,
|
||||
.viewType = resource->viewType,
|
||||
.format = format,
|
||||
};
|
||||
const auto existing = resource->alternateSampledViews.find(key);
|
||||
if (existing != resource->alternateSampledViews.end()) {
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
"for textureId=%d (available=0x%x)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
const VkImageView view = CreateImageView(
|
||||
resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType,
|
||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
resource->alternateSampledViews.emplace(key, view);
|
||||
MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format), resource->sampledBaseMipLevel,
|
||||
resource->sampledBaseMipLevel + resource->sampledLevelCount);
|
||||
return view;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||
Uint32 mipLevel, VkFormat format,
|
||||
Bool layered, Int32 layer) {
|
||||
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||
(resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
format = resource->format;
|
||||
}
|
||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format != resource->format &&
|
||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = resource->arrayLayers;
|
||||
VkImageViewType viewType = resource->viewType;
|
||||
if (!layered) {
|
||||
switch (resource->viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_1D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
__func__, texture.GetExternalIndex());
|
||||
return VK_NULL_HANDLE;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (viewType != resource->viewType) {
|
||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
baseArrayLayer = static_cast<Uint32>(layer);
|
||||
layerCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType;
|
||||
if (format == resource->format && isFullResourceView) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
const TextureResource::StorageImageViewKey key{
|
||||
.mipLevel = mipLevel,
|
||||
.baseArrayLayer = baseArrayLayer,
|
||||
.layerCount = layerCount,
|
||||
.viewType = viewType,
|
||||
.format = format,
|
||||
};
|
||||
auto it = resource->storageImageViews.find(key);
|
||||
if (it != resource->storageImageViews.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
|
||||
if (format != resource->format &&
|
||||
(format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) {
|
||||
requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT;
|
||||
}
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
"(available=0x%x)",
|
||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType,
|
||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
resource->storageImageViews.emplace(key, view);
|
||||
MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return view;
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -910,6 +1156,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ok;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||
const auto it = m_textureResources.find(MakeTextureIdentity(&texture));
|
||||
if (it == m_textureResources.end()) {
|
||||
return true;
|
||||
}
|
||||
const TextureResource& resource = it->second;
|
||||
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
return true;
|
||||
}
|
||||
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||
// path may upload or rebuild, both of which need the render pass ended first.
|
||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||
resource.syncedMipLevelCount != mipLevelCount;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
@@ -1085,6 +1349,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
const Bool supportsStorageImage =
|
||||
!isMultisampleTexture &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||
@@ -1092,6 +1369,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resource.mipLevels == backingMipLevels;
|
||||
if (compatible) {
|
||||
if (resource.perMipViews.size() != backingMipLevels) {
|
||||
@@ -1112,6 +1390,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||
resource.mipLevels < backingMipLevels &&
|
||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -1123,26 +1402,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DeferResourceRelease(Move(resource));
|
||||
}
|
||||
|
||||
auto aspect = GetAspectMaskForFormat(format);
|
||||
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.flags = shapeInfo.imageFlags;
|
||||
imageInfo.imageType = shapeInfo.imageType;
|
||||
imageInfo.flags = imageCreateFlags;
|
||||
imageInfo.imageType = shapeInfo.imageType;
|
||||
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
|
||||
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
|
||||
imageInfo.extent.depth = shapeInfo.depth;
|
||||
imageInfo.extent.depth = shapeInfo.depth;
|
||||
imageInfo.mipLevels = backingMipLevels;
|
||||
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
||||
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
||||
imageInfo.format = format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
const Bool supportsStorageImage =
|
||||
!isMultisampleTexture &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
@@ -1153,15 +1424,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
imageInfo.samples = resolvedSampleCount;
|
||||
if (isMultisampleTexture) {
|
||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
imageInfo.flags, &imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||
// this texture; failing creation would lose the texture entirely, so retry
|
||||
// as a plain immutable-format image.
|
||||
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||
"will be unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
// Remember the verdict so later syncs of same-format textures neither retry
|
||||
// the probe nor flag-mismatch against this image and recreate it.
|
||||
m_mutableFormatUnsupported.insert(format);
|
||||
imageInfo.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
imageCreateFlags = imageInfo.flags;
|
||||
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
imageInfo.flags, &imageFormatProperties);
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS ||
|
||||
(imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) {
|
||||
MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x",
|
||||
__func__, texture.GetSamples(), texture.GetExternalIndex(),
|
||||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||
"format=%d usage=0x%x",
|
||||
__func__, static_cast<Uint32>(imageInfo.flags), texture.GetSamples(),
|
||||
texture.GetExternalIndex(),
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||
return false;
|
||||
@@ -1188,6 +1479,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.aspect = aspect;
|
||||
resource.viewType = shapeInfo.viewType;
|
||||
resource.sampleCount = resolvedSampleCount;
|
||||
resource.imageCreateFlags = imageCreateFlags;
|
||||
resource.syncedTextureParamsVersion = 0;
|
||||
|
||||
if (preservedResource) {
|
||||
@@ -1203,7 +1495,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
||||
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
|
||||
resource.sampledView == VK_NULL_HANDLE &&
|
||||
resource.perMipViews.empty() && resource.perMipSampledViews.empty()) {
|
||||
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
|
||||
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
|
||||
resource.storageImageViews.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1299,6 +1593,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampledView = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
for (const auto& [_, sampledView] : resource.alternateSampledViews) {
|
||||
DeferViewRelease(sampledView);
|
||||
}
|
||||
resource.alternateSampledViews.clear();
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
@@ -1324,7 +1622,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 baseArrayLayer,
|
||||
Uint32 layerCount,
|
||||
const VkComponentMapping* components) const {
|
||||
const VkComponentMapping* components,
|
||||
VkImageUsageFlags viewUsage) const {
|
||||
VkImageViewCreateInfo viewInfo{};
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
viewInfo.image = image;
|
||||
@@ -1340,6 +1639,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
|
||||
viewInfo.subresourceRange.layerCount = layerCount;
|
||||
|
||||
VkImageViewUsageCreateInfo usageInfo{};
|
||||
if (viewUsage != 0) {
|
||||
usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
|
||||
usageInfo.usage = viewUsage;
|
||||
viewInfo.pNext = &usageInfo;
|
||||
}
|
||||
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
|
||||
return view;
|
||||
@@ -1665,4 +1971,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
return imageAspect;
|
||||
}
|
||||
|
||||
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
|
||||
SamplerNumericDomain numericDomain) {
|
||||
// Depth/stencil images always sample through the existing depth-aspect sampledView.
|
||||
// Combined formats (D24S8, D32FS8) are multi-numeric, so vkuFormatIsSampledFloat is
|
||||
// false for them by design, yet their depth aspect reads as float in every GL depth
|
||||
// texture mode; Vulkan also forbids reinterpreting them through color-class views.
|
||||
// Integer domains keep the same view (pre-reinterpretation behavior for stencil-index
|
||||
// style access) rather than failing the draw.
|
||||
if (vkuFormatIsDepthOrStencil(imageFormat)) {
|
||||
return imageFormat;
|
||||
}
|
||||
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
|
||||
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
|
||||
return imageFormat;
|
||||
}
|
||||
if (!IsMutableStorageImageFormat(imageFormat)) {
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
// Preserve component ordering and bit widths. This selects R32_UINT for an R32_SFLOAT
|
||||
// texture sampled by a usampler rather than an arbitrary member (such as
|
||||
// R8G8B8A8_UINT) of Vulkan's broad 32-bit compatibility class.
|
||||
for (Int candidateValue = static_cast<Int>(VK_FORMAT_R4G4_UNORM_PACK8);
|
||||
candidateValue <= static_cast<Int>(VK_FORMAT_ASTC_12x12_SRGB_BLOCK);
|
||||
++candidateValue) {
|
||||
const VkFormat candidate = static_cast<VkFormat>(candidateValue);
|
||||
if (!IsMutableStorageImageFormat(candidate) ||
|
||||
!FormatMatchesSamplerNumericDomain(candidate, numericDomain) ||
|
||||
!HasMatchingColorComponentLayout(imageFormat, candidate) ||
|
||||
!AreSampledImageViewFormatsCompatible(imageFormat, candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If an integer backing is intentionally bit-read through a float sampler, require
|
||||
// a true floating-point view. Normalized/scaled views satisfy OpTypeFloat but apply
|
||||
// an unrelated numeric conversion to those bits.
|
||||
if (numericDomain == SamplerNumericDomain::Float && !vkuFormatIsSFLOAT(candidate)) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
|
||||
if (imageFormat == viewFormat) {
|
||||
return true;
|
||||
}
|
||||
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
|
||||
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||
}
|
||||
|
||||
Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
|
||||
if (imageFormat == viewFormat) {
|
||||
return true;
|
||||
}
|
||||
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
|
||||
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
#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
|
||||
@@ -78,6 +81,61 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView fullView = VK_NULL_HANDLE;
|
||||
@@ -85,6 +143,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,6 +156,7 @@ public:
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
@@ -115,6 +176,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,6 +189,7 @@ 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->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
@@ -153,6 +217,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 +235,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 +250,7 @@ public:
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
@@ -198,6 +275,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,
|
||||
@@ -205,8 +285,16 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
// 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,
|
||||
@@ -255,7 +343,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);
|
||||
@@ -296,6 +385,9 @@ private:
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// 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;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
|
||||
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 {
|
||||
@@ -131,6 +137,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 +171,11 @@ 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);
|
||||
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 +238,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.
|
||||
@@ -235,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
void RecreateSwapchain();
|
||||
// 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 +299,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
|
||||
@@ -329,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
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;
|
||||
@@ -347,8 +379,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.
|
||||
@@ -421,9 +455,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
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();
|
||||
@@ -446,10 +535,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
Bool PrepareStorageImageTextures(
|
||||
VkCommandBuffer commandBuffer,
|
||||
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 +561,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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -60,6 +60,87 @@ 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,
|
||||
@@ -556,6 +637,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 +700,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 +736,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 +1177,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 +1364,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 +1389,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 +1419,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 +1447,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;
|
||||
@@ -1626,8 +1799,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 +1852,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 +2144,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",
|
||||
|
||||
@@ -529,6 +529,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 +1175,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 +1207,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 +1278,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 +1737,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;
|
||||
@@ -1894,7 +1908,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;
|
||||
@@ -1955,6 +1975,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 +1996,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
|
||||
|
||||
@@ -339,6 +339,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,
|
||||
@@ -735,6 +738,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 +793,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) {
|
||||
@@ -900,14 +915,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 +972,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 +1001,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 +1133,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 +1169,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 +1208,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 +1272,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 +1306,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 +1344,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 +1406,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 +1430,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 +1502,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);
|
||||
|
||||
@@ -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;
|
||||
@@ -206,7 +233,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 +276,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 +304,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));
|
||||
}
|
||||
|
||||
@@ -74,6 +74,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateMaxAnisotropy(Float maxAnisotropy, const char* caller) {
|
||||
if (maxAnisotropy >= 1.0f) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
Fn&& fn) {
|
||||
@@ -284,10 +294,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
|
||||
}
|
||||
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
|
||||
// Array targets store their layer count in z; layers never participate in mip
|
||||
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
|
||||
Bool DepthParticipatesInMipmapping(TextureTarget target) {
|
||||
return target == TextureTarget::Texture3D;
|
||||
}
|
||||
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
|
||||
Int maxDimension = std::max<Int>(
|
||||
baseTexelSize.x(),
|
||||
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
|
||||
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
|
||||
Uint mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
@@ -296,11 +312,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
|
||||
return {
|
||||
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
|
||||
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
|
||||
: std::max<Int>(baseTexelSize.z(), 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -323,15 +340,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
|
||||
for (Uint level = 1; level < requiredLevelCount; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level);
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
||||
texture.MarkStorageDirty(uploadTarget, level, false);
|
||||
}
|
||||
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
|
||||
// grows, so a previously longer chain (a bigger base image before respecification) would
|
||||
// otherwise keep a tail of stale levels here and read as incomplete.
|
||||
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
|
||||
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
|
||||
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
|
||||
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
|
||||
@@ -404,18 +426,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"2D multisample textures must use depth 1."));
|
||||
return false;
|
||||
}
|
||||
if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"2D multisample array textures must have at least one layer."));
|
||||
return false;
|
||||
}
|
||||
// Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height
|
||||
// == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative
|
||||
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
|
||||
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
|
||||
|
||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
||||
if (samples > maxSamples) {
|
||||
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
|
||||
// exceeds what the format supports, and the native Adreno driver agrees.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
|
||||
@@ -439,8 +460,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->SetSamples(samples);
|
||||
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
|
||||
// Multisample textures are single-level by definition, so a name that previously held a
|
||||
// mip chain must not keep its tail now that AllocateStorage only grows.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
|
||||
}
|
||||
|
||||
// Redefining level 0 of a texture that already had a base image drops the rest of the chain,
|
||||
// which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
|
||||
// behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
|
||||
// any level-0 respecification leaves the chain in precisely the state it would have had
|
||||
// before, while an upload to level N no longer destroys the levels beneath it.
|
||||
//
|
||||
// Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
|
||||
// Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
|
||||
// level count. A size-only test would leave the old tail in place, and because Mojang
|
||||
// terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
|
||||
// IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
|
||||
// entirely (Managers.cpp) and the atlas samples black.
|
||||
//
|
||||
// The "already has a base image" test is what lets the fix work at all: a level that was
|
||||
// never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
|
||||
// then level 0 - must not discard the levels just uploaded. That ordering is what
|
||||
// KHR-GL33.texture_repeat_mode does.
|
||||
// Scoped to the respecified upload target only, which is what AllocateLevel already did.
|
||||
// Cube maps keep six independent chains while reporting a single level count (face +X), so
|
||||
// respecifying a face other than +X can leave the count longer than that face - but that
|
||||
// asymmetry predates this change and widening the truncation to all six faces would destroy
|
||||
// mip data for faces the application never touched. Left alone deliberately.
|
||||
void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
|
||||
TextureUploadTarget uploadTarget, Uint level) {
|
||||
if (level != 0) return;
|
||||
|
||||
const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
|
||||
const Bool hasExistingBaseImage =
|
||||
existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
|
||||
if (!hasExistingBaseImage) return;
|
||||
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
}
|
||||
|
||||
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
|
||||
// reports 0, so every compressed internalformat is by definition unsupported and
|
||||
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
|
||||
// a C++ exception through the C GL ABI and takes the process down.
|
||||
void RecordUnsupportedCompressedFormat(const char* caller) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Compressed texture formats are not supported."));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||
@@ -455,9 +524,229 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
|
||||
const char* caller) {
|
||||
if (texture == 0) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"Clear texture operations require a non-zero texture name.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto textureObject = GetTextureObjectByName(texture, caller);
|
||||
if (!textureObject) return nullptr;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"Buffer textures cannot be cleared with glClearTexImage.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
|
||||
if (level < 0) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidValue,
|
||||
std::format("Texture level {} is negative.", level));
|
||||
return nullptr;
|
||||
}
|
||||
// ARB_clear_texture: clearing an image that was never defined by TexImage*/
|
||||
// TexStorage* is INVALID_OPERATION, not INVALID_VALUE.
|
||||
if (static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
std::format("Texture level {} is not defined.", level));
|
||||
return nullptr;
|
||||
}
|
||||
return mipmapTexture;
|
||||
}
|
||||
|
||||
Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
|
||||
GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
|
||||
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||
if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
|
||||
!TextureImpl::ValidateTexturePixelDataType(inputType) ||
|
||||
!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
||||
inputFormat, textureObject->GetFormat(), inputType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPixel.clear();
|
||||
if (data == nullptr) {
|
||||
// ARB_clear_texture defines a null clear value as all zeroes. Keeping the
|
||||
// pattern empty lets the region writer use a fast memset path.
|
||||
return true;
|
||||
}
|
||||
|
||||
PixelStoreParameters clearPixelStore{};
|
||||
clearPixelStore.Alignment = 1;
|
||||
SizeT clearPixelSize = 0;
|
||||
void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
|
||||
data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
|
||||
{1, 1, 1}, false, clearPixelSize);
|
||||
if (!converted || clearPixelSize == 0) {
|
||||
if (converted) free(converted);
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPixel.resize(clearPixelSize);
|
||||
Memcpy(clearPixel.data(), converted, clearPixelSize);
|
||||
free(converted);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Writes the clear into the CPU shadow and marks the whole level dirty, exactly like
|
||||
// TexSubImage*_State does. Shared limitation of the level-granular shadow sync: the
|
||||
// shadow does not reflect GPU-side writes (FBO rendering, imageStore), so a PARTIAL
|
||||
// clear of a GPU-written level re-uploads stale shadow bytes outside the region on
|
||||
// the next sync. Full-level clears (glClearTexImage, or a sub-clear covering the
|
||||
// level) rewrite the entire shadow and are always correct.
|
||||
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level,
|
||||
GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
GLsizei width, GLsizei height, GLsizei depth,
|
||||
const Vector<Uint8>& clearPixel, const char* caller) {
|
||||
const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"The requested texture level has no storage.");
|
||||
return false;
|
||||
}
|
||||
if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
|
||||
width < 0 || height < 0 || depth < 0 ||
|
||||
width > texelSize.x() - xoffset ||
|
||||
height > texelSize.y() - yoffset ||
|
||||
depth > texelSize.z() - zoffset) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidValue,
|
||||
"The clear region lies outside the requested texture level.");
|
||||
return false;
|
||||
}
|
||||
if (width == 0 || height == 0 || depth == 0) return true;
|
||||
|
||||
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||
static_cast<SizeT>(texelSize.y()) *
|
||||
static_cast<SizeT>(texelSize.z());
|
||||
const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
|
||||
if (byteSize == 0 || byteSize % texelCount != 0) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"The requested texture storage cannot be cleared.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = byteSize / texelCount;
|
||||
if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
|
||||
RecordClearTextureError(
|
||||
caller, ErrorCode::InvalidOperation,
|
||||
std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
|
||||
clearPixel.size(), bytesPerTexel));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* destination = static_cast<Uint8*>(
|
||||
textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
|
||||
if (!destination) {
|
||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||
"The requested texture level could not be mapped.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
|
||||
const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
|
||||
const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
|
||||
Uint8* firstClearRow = nullptr;
|
||||
|
||||
for (GLsizei z = 0; z < depth; ++z) {
|
||||
for (GLsizei y = 0; y < height; ++y) {
|
||||
Uint8* row = destination +
|
||||
static_cast<SizeT>(zoffset + z) * fullSliceBytes +
|
||||
static_cast<SizeT>(yoffset + y) * fullRowBytes +
|
||||
static_cast<SizeT>(xoffset) * bytesPerTexel;
|
||||
if (firstClearRow) {
|
||||
Memcpy(row, firstClearRow, clearRowBytes);
|
||||
continue;
|
||||
}
|
||||
|
||||
firstClearRow = row;
|
||||
if (clearPixel.empty()) {
|
||||
Memset(row, 0, clearRowBytes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Memcpy(row, clearPixel.data(), bytesPerTexel);
|
||||
SizeT filled = bytesPerTexel;
|
||||
while (filled < clearRowBytes) {
|
||||
const SizeT copySize = std::min(filled, clearRowBytes - filled);
|
||||
Memcpy(row + filled, row, copySize);
|
||||
filled += copySize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
|
||||
auto textureObject = GetClearTextureObject(texture, level, __func__);
|
||||
if (!textureObject) return;
|
||||
|
||||
Vector<Uint8> clearPixel;
|
||||
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
|
||||
|
||||
for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
|
||||
const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
|
||||
size.x(), size.y(), size.z(), clearPixel, __func__)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
auto textureObject = GetClearTextureObject(texture, level, __func__);
|
||||
if (!textureObject) return;
|
||||
|
||||
Vector<Uint8> clearPixel;
|
||||
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
|
||||
|
||||
const auto& uploadTargets = textureObject->GetUploadTargets();
|
||||
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
|
||||
if (zoffset < 0 || depth < 0 ||
|
||||
static_cast<SizeT>(zoffset) > uploadTargets.size() ||
|
||||
static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
|
||||
RecordClearTextureError(__func__, ErrorCode::InvalidValue,
|
||||
"The cube-map clear region selects invalid faces.");
|
||||
return;
|
||||
}
|
||||
for (GLsizei face = 0; face < depth; ++face) {
|
||||
if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
|
||||
xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadTargets.empty()) {
|
||||
RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
|
||||
"The requested texture has no upload target.");
|
||||
return;
|
||||
}
|
||||
ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
|
||||
width, height, depth, clearPixel, __func__);
|
||||
}
|
||||
|
||||
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLenum pname, GLint param, const char* caller) {
|
||||
const auto target = textureObject->GetTarget();
|
||||
if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) {
|
||||
return false;
|
||||
}
|
||||
if ((pname == GL_TEXTURE_BASE_LEVEL || pname == GL_TEXTURE_MAX_LEVEL) && param < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -487,7 +776,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T || pname == GL_TEXTURE_WRAP_R ||
|
||||
pname == GL_TEXTURE_MIN_FILTER || pname == GL_TEXTURE_MAG_FILTER || pname == GL_TEXTURE_MIN_LOD ||
|
||||
pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE ||
|
||||
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR)) {
|
||||
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
|
||||
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
@@ -557,6 +847,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_A: {
|
||||
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
|
||||
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
|
||||
if (swizzleValue == TextureSwizzleParam::Unknown) {
|
||||
// GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
|
||||
// [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
|
||||
return;
|
||||
}
|
||||
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
|
||||
break;
|
||||
}
|
||||
@@ -578,6 +876,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
textureObject->GetSamplerObject()->SetLodBias((GLfloat)param);
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
textureObject->GetSamplerObject()->SetMaxAnisotropy(static_cast<GLfloat>(param));
|
||||
break;
|
||||
case GL_GENERATE_MIPMAP:
|
||||
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != GL_FALSE);
|
||||
break;
|
||||
@@ -594,7 +895,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||
GLfloat param, const char* caller) {
|
||||
if (!textureObject) return;
|
||||
if (!ValidateTextureParameterForTarget(textureObject, pname, static_cast<GLint>(param), caller)) return;
|
||||
if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) return;
|
||||
const GLint validationParam =
|
||||
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast<GLint>(param);
|
||||
if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, caller)) return;
|
||||
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_MAG_FILTER:
|
||||
@@ -640,6 +944,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
textureObject->GetSamplerObject()->SetLodBias(param);
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
|
||||
break;
|
||||
case GL_GENERATE_MIPMAP:
|
||||
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
|
||||
break;
|
||||
@@ -709,6 +1016,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum(
|
||||
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
*params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -734,6 +1044,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
|
||||
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
|
||||
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
|
||||
// clean). Name 0 resolves to the target's real default texture object, so parameters set on
|
||||
// it are stored and queryable like on any texture.
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTargetForParameter(
|
||||
TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
|
||||
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
|
||||
return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
|
||||
}
|
||||
if (textureTarget == TextureTarget::Unknown) {
|
||||
return nullTextureObject;
|
||||
}
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
return activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
}
|
||||
|
||||
void GenerateMipmap_Backend(GLenum target) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
|
||||
}
|
||||
@@ -779,6 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
|
||||
"Texture object here should always be an object with mipmap");
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
|
||||
return;
|
||||
}
|
||||
|
||||
const void* originalPixels = pixels;
|
||||
const auto& pixelUnpackBufferObject =
|
||||
@@ -811,6 +1144,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;
|
||||
const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize;
|
||||
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
||||
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* srcData = static_cast<const Uint8*>(processedPixels);
|
||||
Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
|
||||
if (destData) {
|
||||
@@ -865,11 +1206,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
TextureInternalFormat textureInternalFormat = textureObject->GetFormat();
|
||||
MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex());
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return;
|
||||
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
|
||||
texturePixelDataType))
|
||||
@@ -1027,7 +1368,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
switch (pname) {
|
||||
@@ -1060,6 +1401,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_A: {
|
||||
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
|
||||
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
|
||||
if (swizzleValue == TextureSwizzleParam::Unknown) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
|
||||
return;
|
||||
}
|
||||
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
|
||||
break;
|
||||
}
|
||||
@@ -1083,6 +1430,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
textureObject->GetSamplerObject()->SetLodBias(param);
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (!ValidateMaxAnisotropy(param, __func__)) return;
|
||||
textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
|
||||
break;
|
||||
case GL_GENERATE_MIPMAP:
|
||||
g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
|
||||
break;
|
||||
@@ -1106,7 +1457,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
TextureParameterObject_State(textureObject, pname, param, __func__);
|
||||
@@ -1122,7 +1473,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromFloats(textureObject, params);
|
||||
break;
|
||||
@@ -1130,7 +1481,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
|
||||
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
|
||||
@@ -1153,7 +1504,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromInts(textureObject, params);
|
||||
break;
|
||||
@@ -1161,7 +1512,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
|
||||
return;
|
||||
@@ -1179,7 +1530,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromIntegerInts(textureObject, params);
|
||||
break;
|
||||
@@ -1187,7 +1538,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
|
||||
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
|
||||
@@ -1207,7 +1558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromUnsignedInts(textureObject, params);
|
||||
break;
|
||||
@@ -1218,7 +1569,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
Vec4<TextureSwizzleParam> swizzleParams;
|
||||
@@ -1269,6 +1620,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1310,6 +1663,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1369,6 +1724,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// RGTC is a 2D-only compression scheme, so a 3D target rejects it. This has to be tested on
|
||||
// the raw enum: the RGTC formats resolve to plain R8/RG8/SNORM storage on the way in (see
|
||||
// GLToMG's TextureEnumConverter), so once the internal format is converted there is nothing
|
||||
// left to distinguish them from an ordinary one- or two-channel upload.
|
||||
if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
|
||||
textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
|
||||
(internalformat == GL_COMPRESSED_RED_RGTC1 || internalformat == GL_COMPRESSED_SIGNED_RED_RGTC1 ||
|
||||
internalformat == GL_COMPRESSED_RG_RGTC2 || internalformat == GL_COMPRESSED_SIGNED_RG_RGTC2)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"RGTC compressed formats are invalid for 3D texture targets"));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
|
||||
// GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
|
||||
// GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
|
||||
@@ -1378,6 +1748,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// target and data is not evenly divisible into the number of bytes needed to store in memory a datum
|
||||
// indicated by type.
|
||||
// ======================= Processing ================================
|
||||
textureInternalFormat =
|
||||
MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
|
||||
@@ -1386,6 +1758,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1419,7 +1793,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
|
||||
// Allocate in TextureObject
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
if (isProxy) {
|
||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||
} else {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
}
|
||||
|
||||
if (!originalPixels) {
|
||||
MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
|
||||
@@ -1446,6 +1825,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
|
||||
free(processedPixels);
|
||||
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
|
||||
}
|
||||
|
||||
void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
|
||||
@@ -1500,6 +1880,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1542,6 +1924,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||
} else {
|
||||
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{width, height, 1}, internalBytes});
|
||||
}
|
||||
@@ -1604,6 +1987,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1628,6 +2013,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Texture object here should always be an object with mipmap");
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
if (!isProxy) {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
||||
}
|
||||
|
||||
@@ -1659,8 +2045,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
|
||||
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
|
||||
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
|
||||
// nonzero name that is not an existing buffer object is an error. This is reachable on
|
||||
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
|
||||
// which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
|
||||
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
if (!bufferObject) {
|
||||
if (buffer != 0 && !bufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
@@ -1674,6 +2064,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
|
||||
// silent no-op; the slot is never empty now that every unit/target holds its default.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1694,7 +2086,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
GLboolean IsTexture_State(GLuint texture) {
|
||||
// ======================= Processing ================================
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE;
|
||||
// GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved
|
||||
// name is just GL_FALSE. Probing with the recording validator (as every other Is* entry
|
||||
// point already avoids doing) would leave a spurious INVALID_VALUE behind.
|
||||
return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -1883,6 +2277,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (params) {
|
||||
*params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
|
||||
}
|
||||
break;
|
||||
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
|
||||
if (params) {
|
||||
*params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
|
||||
@@ -2029,6 +2428,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->GetSamplerObject()->GetSamplerCompareFunc());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (params) {
|
||||
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexParameterfv_State",
|
||||
@@ -2262,7 +2666,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
|
||||
// TODO: implement
|
||||
// TODO: implement compressed readback. Reporting success while writing nothing hands
|
||||
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
|
||||
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Texture level is not stored in a compressed format."));
|
||||
}
|
||||
|
||||
void GenTextures_State(GLsizei n, GLuint* textures) {
|
||||
@@ -2300,7 +2710,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint textureName = textures[i];
|
||||
if (textureName == 0) continue;
|
||||
if (!TextureImpl::ValidateTextureName(textureName, true)) continue;
|
||||
if (!MG_State::pGLContext->ValidateTextureName(textureName)) continue;
|
||||
MG_State::pGLContext->MarkTextureObjectForDeletion(textureName);
|
||||
}
|
||||
}
|
||||
@@ -2449,20 +2859,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
||||
const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
GLsizei imageSize, const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
@@ -2472,8 +2882,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
@@ -2483,8 +2893,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload. Until then report the spec error for an
|
||||
// unsupported compressed format rather than throwing - a C++ exception unwinding
|
||||
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
|
||||
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||
@@ -2494,8 +2907,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void BindTexture_State(GLenum target, GLuint texture) {
|
||||
@@ -2507,11 +2920,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
|
||||
|
||||
// Name 0 unbinds the current target from the active texture unit.
|
||||
// GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that
|
||||
// glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
|
||||
// restores the unit/target slot to its initial state.
|
||||
if (texture == 0) {
|
||||
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
|
||||
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
|
||||
bindingSlot.Bind(nullptr);
|
||||
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
|
||||
return;
|
||||
}
|
||||
@@ -2523,6 +2938,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted -
|
||||
// is not a legal bind target in the core profile (no application-generated names), and the error
|
||||
// is INVALID_OPERATION, not INVALID_VALUE.
|
||||
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2530,8 +2948,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
// ======================= Processing ================================
|
||||
Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture);
|
||||
if (!doesTextureExist) {
|
||||
@@ -2551,14 +2967,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ActiveTexture_State(GLenum texture) {
|
||||
// ===================== Error Checking ==============================
|
||||
if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) {
|
||||
// GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 +
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
|
||||
// state reset iterates every advertised combined unit, so rejecting units the
|
||||
// implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
|
||||
// whole test batches. The backend already clamps its advertised value to the state
|
||||
// layer's MAX_TEXTURE_IMAGE_UNITS capacity.
|
||||
Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
maxCombinedUnits = std::min(
|
||||
maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
|
||||
}
|
||||
if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ActiveTexture_State",
|
||||
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got "
|
||||
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got "
|
||||
"invalid enum: 0x{:X}, which may stand for unit {}.",
|
||||
texture, texture - GL_TEXTURE0)));
|
||||
maxCombinedUnits - 1, texture, texture - GL_TEXTURE0)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2827,6 +3254,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
||||
// longer pre-existing chain has to be dropped explicitly.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
@@ -2879,6 +3309,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
@@ -2918,16 +3350,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
|
||||
textureObject->SetInternalFormat(textureInternalFormat);
|
||||
// Array targets keep their layer count constant across levels; only true 3D
|
||||
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||
const GLsizei levelDepth = std::max<GLsizei>(1, depth >> level);
|
||||
const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth;
|
||||
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
|
||||
levelDepth);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
@@ -2957,6 +3394,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
|
||||
}
|
||||
@@ -2971,6 +3409,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
|
||||
}
|
||||
@@ -2986,6 +3425,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
|
||||
}
|
||||
@@ -3141,8 +3581,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
if (texture == 0) {
|
||||
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
|
||||
// target's default texture object (the unit's initial state).
|
||||
for (auto& slot : textureUnit.GetAllBindingSlots()) {
|
||||
slot.Bind(nullptr);
|
||||
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3681,6 +4123,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!textureObject) return;
|
||||
// GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
|
||||
// go through CopyTextureSubImage3D with the face as a layer.
|
||||
const auto target = textureObject->GetTarget();
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::Texture1DArray &&
|
||||
target != TextureTarget::TextureRectangle) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"CopyTextureSubImage2D requires a 2D, 1D-array, or "
|
||||
"rectangle texture."));
|
||||
return;
|
||||
}
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) {
|
||||
CopyTexSubImage2D_Backend(glTarget, level, xoffset, yoffset, x, y, width, height);
|
||||
});
|
||||
}
|
||||
|
||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
|
||||
CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -246,6 +252,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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "ProgramObject.h"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -87,6 +88,19 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// How many consecutive uniform locations a uniform occupies. Array uniforms (opaque
|
||||
// or not) span one location per element so glUniform*v(count > 1) and
|
||||
// glGetUniformLocation("arr[k]") can address elements individually; everything else
|
||||
// spans a single location. TObjectReflection.size only carries the element count for
|
||||
// non-block arrays, so prefer the TType, which is authoritative for both.
|
||||
static MobileGL::Int GetUniformLocationSpan(const glslang::TObjectReflection& uniform) {
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type != nullptr && type->isSizedArray()) {
|
||||
return std::max(1, type->getOuterArraySize());
|
||||
}
|
||||
return std::max(1, uniform.size);
|
||||
}
|
||||
|
||||
static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) {
|
||||
bool inLineComment = false;
|
||||
bool inBlockComment = false;
|
||||
@@ -346,7 +360,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
|
||||
if (!m_program->buildReflection()) {
|
||||
// GL-style reflection naming (GL CTS uniform_block relies on all four):
|
||||
// - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec.
|
||||
// - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a",
|
||||
// "s[1].a", ...) following ARB_program_interface_query rules. Default-block
|
||||
// (loose) uniforms already expand per element without this option.
|
||||
// - AllBlockVariables: every member of an active named block is active even when
|
||||
// no shader statement reads it (ES 3.0/GL 3.3 named-block semantics).
|
||||
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
|
||||
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
|
||||
// every block to std140, so this covers all of them.
|
||||
if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
|
||||
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
|
||||
m_linkStatus = false;
|
||||
m_infoLog = "Build reflection failed.";
|
||||
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
|
||||
@@ -361,8 +386,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (int i = 0; i < m_activeUniformCount; i++) {
|
||||
auto& uniform = m_program->getUniform(i);
|
||||
auto location = uniform.layoutLocation();
|
||||
const Int locationSpan =
|
||||
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
requiredUniformLocations += locationSpan;
|
||||
if (location != glslang::TQualifier::layoutLocationEnd) {
|
||||
m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1);
|
||||
@@ -401,8 +425,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_externalIndex, uniform.name.c_str());
|
||||
continue; // will allocate unallocated uniforms later
|
||||
}
|
||||
const Int locationSpan =
|
||||
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
for (Int element = 0; element < locationSpan; ++element) {
|
||||
m_uniformIndexInTProgram[location + element] = i;
|
||||
}
|
||||
@@ -419,8 +442,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
});
|
||||
for (auto index : unallocatedUniformIndex) {
|
||||
auto& uniform = m_program->getUniform(index);
|
||||
const Int locationSpan =
|
||||
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
Bool placed = false;
|
||||
for (; locNeedle <= m_maxUniformLocation; locNeedle++) {
|
||||
bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation;
|
||||
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
|
||||
@@ -437,8 +460,25 @@ namespace MobileGL::MG_State::GLState {
|
||||
"(index %d)",
|
||||
m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index);
|
||||
locNeedle += locationSpan;
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (!placed) {
|
||||
// Explicit-location uniforms can fragment the space so no contiguous
|
||||
// span is left; grow the table instead of leaving the uniform without
|
||||
// a location (which would make it unsettable via glUniform*).
|
||||
const SizeT base = m_uniformIndexInTProgram.size();
|
||||
m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd);
|
||||
m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
|
||||
m_maxUniformLocation = static_cast<Uint>(base + locationSpan - 1);
|
||||
for (Int element = 0; element < locationSpan; ++element) {
|
||||
m_uniformIndexInTProgram[base + element] = index;
|
||||
}
|
||||
m_uniformLocations[uniform.name] = static_cast<Uint>(base);
|
||||
MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu",
|
||||
m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1);
|
||||
locNeedle = base + locationSpan;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_activeUniformCount; i++) {
|
||||
@@ -454,10 +494,17 @@ namespace MobileGL::MG_State::GLState {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
|
||||
// Reflection names an array "texs[0]" while the layout(binding = N) map from the IO
|
||||
// resolver is keyed by the declared name ("texs"); look up both spellings.
|
||||
auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
|
||||
if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 &&
|
||||
uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) {
|
||||
explicitBinding =
|
||||
m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3));
|
||||
}
|
||||
const int initialUnit =
|
||||
explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast<int>(explicitBinding->second) : 0;
|
||||
const Int locationSpan = std::max(1, uniform.size);
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
for (Int element = 0; element < locationSpan &&
|
||||
location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) {
|
||||
m_uniformSamplerOrImageUnitIndex[location + element] =
|
||||
@@ -622,8 +669,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_uniformSizesInBytes.clear();
|
||||
m_uniformOffsets.clear();
|
||||
m_globalUboScratch.clear();
|
||||
m_uniformOffsets.resize(m_maxUniformLocation + 1);
|
||||
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1);
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset);
|
||||
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0);
|
||||
for (SizeT i = 0; i < m_generatedSpirv.size(); i++) {
|
||||
auto& spv = m_generatedSpirv[i];
|
||||
|
||||
@@ -653,31 +703,95 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
|
||||
m_uniformOffsets[m_uniformLocations[name]] = offset;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u assigned to location %u",
|
||||
m_externalIndex, name.c_str(), offset, m_uniformLocations[name]);
|
||||
} else {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), offset);
|
||||
}
|
||||
}
|
||||
for (const auto& [name, size] : meta.plainUniformMemberSizesInBytes) {
|
||||
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
|
||||
m_uniformSizesInBytes[m_uniformLocations[name]] = size;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u assigned to location %u",
|
||||
m_externalIndex, name.c_str(), size, m_uniformLocations[name]);
|
||||
} else {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), size);
|
||||
}
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
auto locationIt = m_uniformLocations.find(name);
|
||||
if (locationIt == m_uniformLocations.end()) {
|
||||
locationIt = m_uniformLocations.find(name + "[0]");
|
||||
}
|
||||
if (locationIt == m_uniformLocations.end()) {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
const Uint baseLocation = locationIt->second;
|
||||
if (!IsValidUniformLocation(static_cast<Int>(baseLocation))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Int uniformIndex = m_uniformIndexInTProgram[baseLocation];
|
||||
const GLint arraySize = GetActiveUniformArraySize(uniformIndex);
|
||||
SizeT memberSize = 0;
|
||||
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
|
||||
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
|
||||
memberSize = sizeIt->second;
|
||||
}
|
||||
Uint arrayStride = 0;
|
||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||
arrayStride = strideIt->second;
|
||||
}
|
||||
|
||||
// Array uniforms span one location per element (see DoReflection);
|
||||
// give each element its real byte offset inside the UBO.
|
||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||
for (GLint element = 0; element < elementCount; ++element) {
|
||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||
if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) {
|
||||
break;
|
||||
}
|
||||
m_uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
|
||||
m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned "
|
||||
"to locations %u..%u",
|
||||
m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
|
||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata",
|
||||
m_externalIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||
for (Uint location = 0; location <= m_maxUniformLocation; ++location) {
|
||||
if (m_uniformOffsets[location] != kInvalidUniformOffset) continue;
|
||||
if (!IsValidUniformLocation(static_cast<Int>(location))) continue;
|
||||
const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type != nullptr && type->isOpaque()) continue;
|
||||
if (uniform.index >= 0 && uniform.index < m_program->getNumUniformBlocks() &&
|
||||
std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(),
|
||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||
// Member of a named uniform block: not settable through glUniform*, so it
|
||||
// needs no global-UBO shadow storage.
|
||||
continue;
|
||||
}
|
||||
|
||||
// std140-style slot: the matrix upload paths write column vectors at
|
||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||
}
|
||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||
const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||
m_globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||
m_uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||
m_uniformSizesInBytes[location] = slotSize;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the "
|
||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||
m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramObject::WaitUntilGenerationCompleted() const {
|
||||
|
||||
@@ -18,6 +18,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
public:
|
||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
|
||||
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
|
||||
// defers the actual removal to the next link).
|
||||
Bool ShaderIsAttachedGLVisible(const SharedPtr<ShaderObject>& shader) const {
|
||||
const auto matches = [&shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); };
|
||||
if (std::none_of(m_shaders.begin(), m_shaders.end(), matches)) return false;
|
||||
return std::none_of(m_detachedShaders.begin(), m_detachedShaders.end(), matches);
|
||||
}
|
||||
bool AttachShader(const SharedPtr<ShaderObject>& shader);
|
||||
SizeT DetachShader(const SharedPtr<ShaderObject>& shader);
|
||||
SizeT RemoveShader(const SharedPtr<ShaderObject>& shader);
|
||||
@@ -43,8 +50,51 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
const auto it = m_uniformLocations.find(name);
|
||||
if (it == m_uniformLocations.end()) return -1;
|
||||
return (Int)it->second;
|
||||
if (it != m_uniformLocations.end()) return (Int)it->second;
|
||||
|
||||
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
|
||||
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
|
||||
// to base + k because DoReflection reserves one location per array element.
|
||||
if (name.empty()) return -1;
|
||||
if (name.back() != ']') {
|
||||
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
|
||||
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
|
||||
return -1;
|
||||
}
|
||||
if (name.length() < 4) return -1;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
// Require at least one digit between the brackets.
|
||||
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return -1;
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0FFFFFFFu) return -1;
|
||||
}
|
||||
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
|
||||
if (baseIt == m_uniformLocations.end()) {
|
||||
// Legacy key without the "[0]" suffix (defensive; reflection normally
|
||||
// stores the suffixed form for arrays).
|
||||
baseIt = m_uniformLocations.find(name.substr(0, bracket));
|
||||
if (baseIt == m_uniformLocations.end()) return -1;
|
||||
}
|
||||
const Int base = (Int)baseIt->second;
|
||||
if (!IsValidUniformLocation(base)) return -1;
|
||||
const Int index = m_uniformIndexInTProgram[base];
|
||||
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
||||
// in-range elements.
|
||||
const glslang::TType* type = m_program->getUniform(index).getType();
|
||||
if (type == nullptr || !type->isArray()) return -1;
|
||||
if (static_cast<GLint>(element) >= GetActiveUniformArraySize(index)) return -1;
|
||||
const Int location = base + (Int)element;
|
||||
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
||||
return location;
|
||||
}
|
||||
|
||||
// True when both locations are element slots of the same uniform variable.
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
|
||||
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
|
||||
}
|
||||
|
||||
Int GetActiveUniformIndex(const String& name) const {
|
||||
@@ -54,6 +104,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
return uniformIndex;
|
||||
}
|
||||
|
||||
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
||||
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
|
||||
// robustness against non-suffixed reflection entries.
|
||||
if (!name.empty() && name.back() != ']') {
|
||||
const String suffixedName = name + "[0]";
|
||||
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
|
||||
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
|
||||
m_program->getUniform(suffixedIndex).name == suffixedName) {
|
||||
return suffixedIndex;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
||||
const String baseName = name.substr(0, name.length() - 3);
|
||||
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
|
||||
@@ -104,11 +167,24 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
|
||||
// block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the
|
||||
// default-block case, so gate on block membership to return the spec-mandated -1.
|
||||
// block member; -1 for a default-block uniform (glslang yields arrayStride==0 there, so gate
|
||||
// on block membership for the spec-mandated -1). The stride itself is derived from the type
|
||||
// instead of glslang's reflected arrayStride: for an array nested inside a struct member,
|
||||
// glslang computes that field against the enclosing STRUCT's (unset) packing and reports a
|
||||
// tight std430-like stride (ivec2 a[7] -> 8), even though its own member offsets and the
|
||||
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
||||
// layout is always std140, where every array element stride rounds up to a vec4.
|
||||
GLint GetActiveUniformArrayStride(Uint index) const {
|
||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
||||
return (uniform.index < 0) ? -1 : uniform.arrayStride;
|
||||
if (uniform.index < 0) return -1;
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type == nullptr || !type->isArray()) return 0;
|
||||
if (type->isMatrix()) {
|
||||
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
||||
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols();
|
||||
return GetActiveUniformMatrixStride(index) * vectors;
|
||||
}
|
||||
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
|
||||
}
|
||||
|
||||
// GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The
|
||||
@@ -170,6 +246,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
||||
return uniform.name;
|
||||
}
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; }
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
|
||||
@@ -292,6 +371,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
auto it = m_uniformBlockIndexByName.find(name);
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
|
||||
// a bare "Block" query resolves to the first instance per GL semantics.
|
||||
const String suffixedName = String(name) + "[0]";
|
||||
it = m_uniformBlockIndexByName.find(suffixedName);
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
return 0xFFFFFFFFu; // GL_INVALID_INDEX
|
||||
}
|
||||
Bool IsActiveUniformBlock(Uint index) const {
|
||||
@@ -300,7 +384,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsActiveUniformBlock(index)) return 0;
|
||||
return m_program->getUniformBlock((Int)index).size;
|
||||
// glslang reports the unpadded end offset of the last member, but a std140 block
|
||||
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
||||
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
||||
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
||||
return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u;
|
||||
}
|
||||
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
@@ -308,8 +396,30 @@ namespace MobileGL::MG_State::GLState {
|
||||
return ubo.name;
|
||||
}
|
||||
|
||||
// Uniform entries that belong to an arrayed uniform block are reflected once, against
|
||||
// the first instance ("Block[0]"); per GL semantics every other instance shares that
|
||||
// member set. Maps any instance's block index to the index owning the member entries.
|
||||
Uint GetUniformBlockMemberOwnerIndex(Uint index) const {
|
||||
const String& name = GetUniformBlockName(index);
|
||||
if (name.empty() || name.back() != ']') return index;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos) return index;
|
||||
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
return index;
|
||||
}
|
||||
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: derived from the same active-uniform scan that
|
||||
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
|
||||
// (glslang's numMembers counts declared members, which diverges from the reflected
|
||||
// entry list for struct arrays and arrayed block instances).
|
||||
Int GetUniformBlockActiveUniformCount(Uint index) const {
|
||||
return m_program->getUniformBlock((Int)index).numMembers;
|
||||
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
|
||||
Int count = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
|
||||
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
||||
|
||||
@@ -29,9 +29,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||
auto& programObject = m_programObjects[program];
|
||||
if (programObject != nullptr) {
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject->MarkAsDeleted();
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +75,35 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||
auto& shaderObject = m_shaderObjects[shader];
|
||||
if (shaderObject != nullptr) {
|
||||
m_shaderObjects[shader]->MarkAsDeleted();
|
||||
m_shaderObjects[shader].reset();
|
||||
m_shaderIndexGenerator.Delete(shader);
|
||||
// glDeleteShader on an attached shader only FLAGS it; the name stays valid (and
|
||||
// glShaderSource/glCompileShader keep working on it) until the shader is detached
|
||||
// from every program. The GL CTS compiles shaders through exactly this
|
||||
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
|
||||
shaderObject->MarkAsDeleted();
|
||||
ReleaseShaderNameIfOrphaned(shader);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const {
|
||||
for (const auto& programObject : m_programObjects) {
|
||||
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A program deleted while current vacates its table slot but stays alive as the
|
||||
// current program; its attachments still count.
|
||||
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject);
|
||||
}
|
||||
|
||||
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) {
|
||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||
auto& shaderObject = m_shaderObjects[shader];
|
||||
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
||||
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
|
||||
shaderObject.reset();
|
||||
m_shaderIndexGenerator.Delete(shader);
|
||||
}
|
||||
|
||||
Bool ProgramState::ValidateShaderObject(Uint shader) const {
|
||||
return CheckIndexAvail(shader, m_shaderObjects) && m_shaderObjects[shader] != nullptr;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint CreateShader(ShaderStage stage);
|
||||
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
|
||||
void MarkShaderObjectForDeletion(Uint shader);
|
||||
// Frees a deletion-flagged shader's name once no program holds a GL-visible
|
||||
// attachment to it (the deferred half of glDeleteShader-while-attached).
|
||||
void ReleaseShaderNameIfOrphaned(Uint shader);
|
||||
Bool ValidateShaderObject(Uint shader) const;
|
||||
|
||||
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
|
||||
|
||||
private:
|
||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
||||
|
||||
template <typename T>
|
||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
||||
return idx < vec.size();
|
||||
|
||||
@@ -78,6 +78,13 @@ namespace MobileGL {
|
||||
++m_version;
|
||||
}
|
||||
|
||||
void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) {
|
||||
if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return;
|
||||
|
||||
m_samplerParameters.maxAnisotropy = maxAnisotropy;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) {
|
||||
if (func == m_samplerParameters.compareFunc) return;
|
||||
|
||||
@@ -128,6 +135,10 @@ namespace MobileGL {
|
||||
return m_samplerParameters.lodBias;
|
||||
}
|
||||
|
||||
Float SamplerObject::GetMaxAnisotropy() const {
|
||||
return m_samplerParameters.maxAnisotropy;
|
||||
}
|
||||
|
||||
SamplerCompareMode SamplerObject::GetCompareMode() const {
|
||||
return m_samplerParameters.compareMode;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ namespace MobileGL {
|
||||
Float minLod = -1000.0f;
|
||||
Float maxLod = 1000.0f;
|
||||
Float lodBias = 0.0f;
|
||||
Float maxAnisotropy = 1.0f;
|
||||
SamplerCompareFunc compareFunc = SamplerCompareFunc::Always;
|
||||
SamplerCompareMode compareMode = SamplerCompareMode::None;
|
||||
};
|
||||
@@ -83,6 +84,7 @@ namespace MobileGL {
|
||||
void SetMipmapMode(SamplerMipmapMode mode);
|
||||
void SetLodRange(Float minLod, Float maxLod);
|
||||
void SetLodBias(Float bias);
|
||||
void SetMaxAnisotropy(Float maxAnisotropy);
|
||||
void SetSamplerCompareFunc(SamplerCompareFunc func);
|
||||
void SetCompareMode(SamplerCompareMode mode);
|
||||
|
||||
@@ -95,6 +97,7 @@ namespace MobileGL {
|
||||
Float GetMinLod() const;
|
||||
Float GetMaxLod() const;
|
||||
Float GetLodBias() const;
|
||||
Float GetMaxAnisotropy() const;
|
||||
SamplerCompareMode GetCompareMode() const;
|
||||
SamplerCompareFunc GetSamplerCompareFunc() const;
|
||||
Uint GetExternalIndex() const;
|
||||
|
||||
@@ -16,17 +16,32 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
|
||||
m_data.reserve(std::bit_ceil(level + 1));
|
||||
m_data.resize(level + 1);
|
||||
m_texelSizes.reserve(std::bit_ceil(level + 1));
|
||||
m_texelSizes.resize(level + 1);
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
m_isDirty.resize(level + 1, false);
|
||||
// Grow only. GL respecifies exactly the level it is handed, so allocating level 0
|
||||
// must not disturb the levels above it - but resize() shrinks as readily as it
|
||||
// grows, so this used to truncate the whole chain to a single level. Callers that
|
||||
// genuinely redefine the complete level set say so with TruncateToLevelCount.
|
||||
const SizeT requiredLevelCount = static_cast<SizeT>(level) + 1;
|
||||
if (m_data.size() < requiredLevelCount) {
|
||||
m_data.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_data.resize(requiredLevelCount);
|
||||
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
if (levelCount >= m_data.size()) return;
|
||||
|
||||
m_data.resize(levelCount);
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
auto& targetData = m_data;
|
||||
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
||||
@@ -55,6 +70,7 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
SizeT MipmapStorage::GetByteSize(Uint level) const {
|
||||
if (level >= m_data.size()) return 0;
|
||||
return m_data[level].size();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace MobileGL {
|
||||
public:
|
||||
SizeT GetLevelCount() const;
|
||||
void AllocateLevel(Uint level, MipmapInput input);
|
||||
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
|
||||
// is the only way a chain gets shorter - use it where the caller defines the whole
|
||||
// level set (glTexStorage*, mip regeneration, atlas respecification).
|
||||
void TruncateToLevelCount(SizeT levelCount);
|
||||
void UpdateSubData(Uint level, DataPtr input);
|
||||
void* MapData(Uint level);
|
||||
IntVec3 GetTexelSize(Uint level) const;
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace MobileGL {
|
||||
m_storage[targetIndex].AllocateLevel(level, input);
|
||||
}
|
||||
|
||||
// Per-target, like AllocateLevel: cube-map faces are respecified independently, so
|
||||
// truncating one face must not disturb the others.
|
||||
void TruncateToLevelCount(Uint targetIndex, SizeT levelCount) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "TruncateToLevelCount: target invalid");
|
||||
|
||||
m_storage[targetIndex].TruncateToLevelCount(levelCount);
|
||||
}
|
||||
|
||||
void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid");
|
||||
m_storage[targetIndex].UpdateSubData(level, input);
|
||||
|
||||
@@ -76,6 +76,14 @@ namespace MobileGL {
|
||||
BGRInteger,
|
||||
RGBAInteger,
|
||||
BGRAInteger,
|
||||
// Desktop-GL single-channel client formats (table 3.3): the data holds one component that
|
||||
// feeds the G, B or A channel; the remaining channels default to 0 (color) / 1 (alpha).
|
||||
Green,
|
||||
Blue,
|
||||
Alpha,
|
||||
GreenInteger,
|
||||
BlueInteger,
|
||||
AlphaInteger,
|
||||
StencilIndex,
|
||||
DepthComponent,
|
||||
DepthStencil,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "TextureObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
@@ -52,6 +53,20 @@ namespace MobileGL {
|
||||
void TextureObjectBase::SetInternalFormat(TextureInternalFormat format) {
|
||||
if (format == m_internalFormat) return;
|
||||
|
||||
// A default texture (name 0) changes IsUndefinedDefaultTexture on the
|
||||
// Unknown<->defined transition, which changes per-draw sampled-set membership
|
||||
// without any bind happening; bump the bind generation so cached sampled sets
|
||||
// re-resolve instead of replaying the stale membership. The identity check
|
||||
// excludes the other externalIndex-0 objects (proxy textures, default-FBO
|
||||
// attachments) whose definedness never feeds sampled-set membership, so e.g.
|
||||
// proxy probes cannot churn the cache.
|
||||
if (m_externalIndex == 0 && pGLContext &&
|
||||
(m_internalFormat == TextureInternalFormat::Unknown) !=
|
||||
(format == TextureInternalFormat::Unknown) &&
|
||||
pGLContext->GetDefaultTextureObject(GetTarget()).get() == this) {
|
||||
pGLContext->BumpTextureBindGeneration();
|
||||
}
|
||||
|
||||
m_internalFormat = format;
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
@@ -256,6 +271,10 @@ namespace MobileGL {
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
DataPtr input) {
|
||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
|
||||
@@ -134,6 +134,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||
virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||
virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
|
||||
// AllocateStorage only ever grows the chain. Callers that define the complete level set -
|
||||
// glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the
|
||||
// leftovers explicitly, so a stale tail can never make the texture silently incomplete.
|
||||
virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0;
|
||||
virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
|
||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||
@@ -154,6 +158,17 @@ namespace MobileGL::MG_State::GLState {
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// The per-target default texture objects (name 0) sit permanently in every texture unit's
|
||||
// binding slots, so "nothing useful bound" is no longer a null slot. While a default texture
|
||||
// has never been given an image (its internal format is still Unknown) it can contribute
|
||||
// nothing to sampling; backends treat such a binding exactly like the old empty slot and
|
||||
// skip per-draw sync/bind work for it. Once an application defines an image on a default
|
||||
// texture it loses this shortcut and is synced like any other texture.
|
||||
inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) {
|
||||
return texture != nullptr && texture->GetExternalIndex() == 0 &&
|
||||
texture->GetFormat() == TextureInternalFormat::Unknown;
|
||||
}
|
||||
|
||||
class TextureObjectWithOneMipmap : public TextureObjectMipmap {
|
||||
public:
|
||||
TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
|
||||
@@ -164,6 +179,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
|
||||
@@ -31,6 +31,10 @@ namespace MobileGL {
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
DataPtr input) {
|
||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace MobileGL {
|
||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||
|
||||
@@ -18,9 +18,51 @@
|
||||
#include "TextureObjectStubs.h"
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return MakeShared<TextureObject1D>(index);
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return MakeShared<TextureObject2DCube>(index);
|
||||
case TextureTarget::Texture2D:
|
||||
return MakeShared<TextureObject2D>(index);
|
||||
case TextureTarget::Texture3D:
|
||||
return MakeShared<TextureObject3D>(index);
|
||||
case TextureTarget::TextureBuffer:
|
||||
return MakeShared<TextureObjectBuffer>(index);
|
||||
|
||||
// These texture types are still stubbed:
|
||||
case TextureTarget::TextureRectangle:
|
||||
return MakeShared<TextureObjectRectangle>(index);
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return MakeShared<TextureObject2DMultisample>(index);
|
||||
case TextureTarget::Texture1DArray:
|
||||
return MakeShared<TextureObject1DArray>(index);
|
||||
case TextureTarget::Texture2DArray:
|
||||
return MakeShared<TextureObject2DArray>(index);
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return MakeShared<TextureObjectCubeMapArray>(index);
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return MakeShared<TextureObject2DMultisampleArray>(index);
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
TextureState::TextureState() : m_indexGenerator(1024, 1) {
|
||||
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
||||
// shared across all texture units, and it is the initial binding of every unit/target
|
||||
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
|
||||
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
|
||||
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
||||
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
|
||||
}
|
||||
for (int i = 0; i < MAX_TEXTURE_IMAGE_UNITS; ++i) {
|
||||
m_textureUnits[i] = TextureUnit();
|
||||
for (auto& bindingSlot : m_textureUnits[i].GetAllBindingSlots()) {
|
||||
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +75,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
return nullTextureObject;
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
|
||||
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
|
||||
"GetDefaultTextureObject: invalid texture target %d", (int)target);
|
||||
return m_defaultTextureObjects[(int)target];
|
||||
}
|
||||
|
||||
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) {
|
||||
textures.resize(number);
|
||||
m_indexGenerator.Generate(number, textures.data());
|
||||
@@ -40,52 +88,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) {
|
||||
auto& textureObject = m_textureObjects[index];
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
textureObject = MakeShared<TextureObject1D>(index);
|
||||
break;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
textureObject = MakeShared<TextureObject2DCube>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2D:
|
||||
textureObject = MakeShared<TextureObject2D>(index);
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
textureObject = MakeShared<TextureObject3D>(index);
|
||||
break;
|
||||
case TextureTarget::TextureBuffer:
|
||||
textureObject = MakeShared<TextureObjectBuffer>(index);
|
||||
break;
|
||||
|
||||
// These texture types are still stubbed:
|
||||
case TextureTarget::TextureRectangle:
|
||||
textureObject = MakeShared<TextureObjectRectangle>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
textureObject = MakeShared<TextureObject2DMultisample>(index);
|
||||
break;
|
||||
case TextureTarget::Texture1DArray:
|
||||
textureObject = MakeShared<TextureObject1DArray>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DArray:
|
||||
textureObject = MakeShared<TextureObject2DArray>(index);
|
||||
break;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
textureObject = MakeShared<TextureObjectCubeMapArray>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
textureObject = MakeShared<TextureObject2DMultisampleArray>(index);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
||||
textureObject = MakeTextureObjectForTarget(index, target);
|
||||
if (!textureObject) {
|
||||
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
||||
return nullTextureObject;
|
||||
}
|
||||
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
void TextureState::MarkTextureObjectForDeletion(Uint index) {
|
||||
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
|
||||
if (m_indexGenerator.IsValid(index)) {
|
||||
auto it = m_textureObjects.find(index);
|
||||
if (it != m_textureObjects.end()) {
|
||||
@@ -94,7 +105,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
|
||||
for (auto& bindingSlot : bindingSlots) {
|
||||
if (bindingSlot.GetBoundObject() == it->second) {
|
||||
bindingSlot.Bind(nullptr);
|
||||
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
|
||||
// target's default texture object, on every unit it was bound to.
|
||||
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,8 +122,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
// re-resolved instead of dangling.
|
||||
BumpTextureBindGeneration();
|
||||
m_textureObjects.erase(index);
|
||||
m_indexGenerator.Delete(index);
|
||||
} else if (!keepUnboundReservation) {
|
||||
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
|
||||
// reserved it and no bind ever instantiated an object (so a later bind of it must
|
||||
// fail), and the reservation has to return to the free list.
|
||||
m_indexGenerator.Delete(index);
|
||||
}
|
||||
m_indexGenerator.Delete(index);
|
||||
// Relaxed semantics: legacy apps may delete a generated name before its first
|
||||
// bind, then bind and populate that same name. Keep such a reservation alive there;
|
||||
// a real texture object reaching deletion still releases its index above.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,12 +49,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
void GenerateNames(Uint number, Vector<Uint>& textures);
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
|
||||
// zero names a real, per-target texture object shared by every texture unit; binding 0
|
||||
// binds it, and image/parameter calls on it must work like on any texture. It is not a
|
||||
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE
|
||||
// and by-name lookups keep failing for 0) and can never be deleted.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
TextureUnit& GetUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
||||
Int GetActiveTextureUnit() const;
|
||||
void SetActiveTextureUnit(Int unit);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
|
||||
Bool ValidateName(Uint index) const;
|
||||
Bool ValidateTextureObject(Uint index) const;
|
||||
|
||||
@@ -83,5 +89,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
|
||||
IndexGenerator<Uint> m_indexGenerator;
|
||||
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
|
||||
// One default texture object (external name 0) per target, created with the context and
|
||||
// immortal for its lifetime; the initial binding of every unit/target slot.
|
||||
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -40,10 +40,16 @@ if (APPLE)
|
||||
target_link_libraries(DirectVulkanSanityTest PRIVATE objc)
|
||||
target_link_libraries(DirectVulkanSanityTest PRIVATE ${QUARTZCORE_FRAMEWORK})
|
||||
endif()
|
||||
if (MSVC)
|
||||
# The GLES headers declare gl* as dllimport on Windows, so objects like GetProcAddress.cpp
|
||||
# reference __imp_gl*. Those resolve against the in-library GL entry-point definitions only
|
||||
# if their objects are part of the link; pull the whole static library like the DLL does.
|
||||
target_link_options(DirectVulkanSanityTest PRIVATE /WHOLEARCHIVE:MobileGL_s)
|
||||
endif()
|
||||
target_compile_definitions(DirectVulkanSanityTest PRIVATE -DNOMINMAX)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration)
|
||||
|
||||
add_executable(
|
||||
DirectVulkanTestExec
|
||||
|
||||
@@ -9,8 +9,13 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
|
||||
@@ -22,12 +27,25 @@ namespace {
|
||||
struct FakeDriverState {
|
||||
// Behavior knobs, configured per test before running the probe.
|
||||
GLint maxVertexSsboBlocks = 4;
|
||||
GLint glesMajorVersion = 3;
|
||||
GLint glesMinorVersion = 1;
|
||||
GLint maxVertexImageUniforms = 2;
|
||||
GLint maxGeometryImageUniforms = 3;
|
||||
GLint maxFragmentImageUniforms = 4;
|
||||
GLint maxComputeImageUniforms = 5;
|
||||
bool maxGeometryImageUniformsQueried = false;
|
||||
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
||||
// baseInstance word and exposes it through gl_InstanceID.
|
||||
bool drawLeaksBaseInstanceWord = false;
|
||||
GLenum errorRaisedByDraw = GL_NO_ERROR;
|
||||
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
std::vector<std::string> extensions;
|
||||
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT the fake reports, and whether it was ever asked:
|
||||
// querying it on a driver without the extension would raise GL_INVALID_ENUM.
|
||||
GLfloat maxTextureMaxAnisotropy = 16.0f;
|
||||
bool maxTextureMaxAnisotropyQueried = false;
|
||||
|
||||
GLuint nextBufferId = 1;
|
||||
GLuint nextShaderId = 1;
|
||||
@@ -77,16 +95,29 @@ namespace {
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxVertexSsboBlocks;
|
||||
break;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxVertexImageUniforms;
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||
g_fake.maxGeometryImageUniformsQueried = true;
|
||||
*data = g_fake.maxGeometryImageUniforms;
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxFragmentImageUniforms;
|
||||
break;
|
||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxComputeImageUniforms;
|
||||
break;
|
||||
// FillInGLESCapabilities reads the context version before running the
|
||||
// baseInstance probe, which requires ES >= 3.1.
|
||||
case GL_MAJOR_VERSION:
|
||||
*data = 3;
|
||||
*data = g_fake.glesMajorVersion;
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*data = 1;
|
||||
*data = g_fake.glesMinorVersion;
|
||||
break;
|
||||
case GL_NUM_EXTENSIONS:
|
||||
*data = 0;
|
||||
*data = static_cast<GLint>(g_fake.extensions.size());
|
||||
break;
|
||||
default:
|
||||
// Leave the caller's defaults for every other capability query.
|
||||
@@ -114,11 +145,16 @@ namespace {
|
||||
return reinterpret_cast<const GLubyte*>("");
|
||||
}
|
||||
};
|
||||
// GL_NUM_EXTENSIONS reports 0 above, so this is never reached; it exists so the
|
||||
// table stays complete if the extension loop ever runs.
|
||||
funcs.glGetStringi = [](GLenum, GLuint) -> const GLubyte* { return nullptr; };
|
||||
funcs.glGetStringi = [](GLenum name, GLuint index) -> const GLubyte* {
|
||||
if (name != GL_EXTENSIONS || index >= g_fake.extensions.size()) return nullptr;
|
||||
return reinterpret_cast<const GLubyte*>(g_fake.extensions[index].c_str());
|
||||
};
|
||||
funcs.glGetFloatv = [](GLenum pname, GLfloat* data) {
|
||||
switch (pname) {
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
g_fake.maxTextureMaxAnisotropyQueried = true;
|
||||
data[0] = g_fake.maxTextureMaxAnisotropy;
|
||||
break;
|
||||
// Two-component range queries.
|
||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
@@ -400,3 +436,90 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
|
||||
EXPECT_FALSE(conformingCaps.IndirectDrawInstanceIdIncludesBaseInstance);
|
||||
ExpectProbeReleasedAllObjects();
|
||||
}
|
||||
|
||||
TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGeometry) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es31Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
||||
EXPECT_EQ(es31Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
|
||||
EXPECT_EQ(es31Caps.MaxGeometryImageUniforms, 0);
|
||||
EXPECT_EQ(es31Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
|
||||
EXPECT_EQ(es31Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
|
||||
EXPECT_FALSE(g_fake.maxGeometryImageUniformsQueried);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 2;
|
||||
MobileGL::MG_External::GLESCapabilities es32Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
||||
EXPECT_EQ(es32Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
|
||||
EXPECT_EQ(es32Caps.MaxGeometryImageUniforms, g_fake.maxGeometryImageUniforms);
|
||||
EXPECT_EQ(es32Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
|
||||
EXPECT_EQ(es32Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
|
||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||
}
|
||||
|
||||
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities absentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
|
||||
// Never probed (it would be GL_INVALID_ENUM), and reported as "no anisotropy".
|
||||
EXPECT_FALSE(g_fake.maxTextureMaxAnisotropyQueried);
|
||||
EXPECT_FLOAT_EQ(absentCaps.MaxTextureMaxAnisotropy, 1.0f);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.maxTextureMaxAnisotropy = 16.0f;
|
||||
g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic");
|
||||
MobileGL::MG_External::GLESCapabilities presentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs));
|
||||
EXPECT_TRUE(g_fake.maxTextureMaxAnisotropyQueried);
|
||||
EXPECT_FLOAT_EQ(presentCaps.MaxTextureMaxAnisotropy, 16.0f);
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities absentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
|
||||
EXPECT_FALSE(absentCaps.SupportsTextureFilterAnisotropy);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic");
|
||||
MobileGL::MG_External::GLESCapabilities presentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs));
|
||||
EXPECT_TRUE(presentCaps.SupportsTextureFilterAnisotropy);
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <Config.h>
|
||||
@@ -20,9 +22,31 @@ using namespace MobileGL;
|
||||
|
||||
class BufferTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
// GL error flags are sticky per error code and the context outlives an individual test in this
|
||||
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
|
||||
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
|
||||
static void DrainPendingGlErrors() {
|
||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {}
|
||||
// The call under test must raise exactly the expected error and nothing more: a second pending
|
||||
// error means one entry point queued several, which GetError() would hand out at an unrelated
|
||||
// call site later on.
|
||||
static void ExpectSingleGlError(GLenum expected) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BufferTest, Binding) {
|
||||
@@ -105,6 +129,53 @@ TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 3.3 core 2.9 name lifecycle. The same three rules are asserted per object family (see the
|
||||
// texture/vertex-array/framebuffer/renderbuffer suites): a deleted or never-generated name is
|
||||
// INVALID_OPERATION to bind, deleting one is silent, and a generated-but-never-bound reservation
|
||||
// is still released so the name gets recycled.
|
||||
TEST_F(BufferTest, DeleteOfUnknownOrAlreadyDeletedBufferNameIsSilent) {
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
ASSERT_NE(buffer, 0u);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Double delete, name 0 and a never-generated name must all be ignored without an error.
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
|
||||
MG_Impl::GLImpl::DeleteBuffers(2, unknownNames);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, DeleteGeneratedButUnboundBufferNameReleasesReservationAndBindFails) {
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
ASSERT_NE(buffer, 0u);
|
||||
ASSERT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer));
|
||||
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(buffer));
|
||||
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &recycled);
|
||||
EXPECT_EQ(recycled, buffer);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, BindNeverGeneratedBufferNameIsInvalidOperation) {
|
||||
// Not a small literal: other tests in this binary share the context and generate names in
|
||||
// bulk, so a low number may well be a legitimately reserved name here.
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, std::numeric_limits<GLuint>::max());
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, AcquireMemory) {
|
||||
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||
Vector<Uint> bufferNames;
|
||||
|
||||
@@ -24,4 +24,4 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -62,7 +62,7 @@ set(LINK_LIBRARIES
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
add_subdirectory(BackendLoader)
|
||||
add_subdirectory(Buffer)
|
||||
@@ -72,6 +72,8 @@ add_subdirectory(Texture)
|
||||
add_subdirectory(VertexArray)
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Query)
|
||||
add_subdirectory(Pipeline)
|
||||
add_subdirectory(ShaderTranspiler)
|
||||
if (ENABLE_INTEGRATION_TESTS)
|
||||
add_subdirectory(Backend/DirectVulkan)
|
||||
endif()
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -102,3 +102,33 @@ TEST(EGLStateMakeCurrent, SameThreadRepeatedAttachReleaseDoesNotLeaveStaleOwner)
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
|
||||
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
|
||||
}
|
||||
|
||||
// The compatibility-profile accessor is affirmative-only (it backs GL_CONTEXT_PROFILE_MASK
|
||||
// reporting): attrib-less contexts (profile mask 0) and released threads both answer "not
|
||||
// compat" and therefore read as core-profile contexts.
|
||||
TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
|
||||
auto fixture = CreateFixture();
|
||||
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
|
||||
const EGLint compatAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_MINOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||
EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
|
||||
EGL_NONE};
|
||||
const auto compatContext =
|
||||
fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, compatAttribs);
|
||||
ASSERT_NE(compatContext, EGL_NO_CONTEXT);
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, compatContext));
|
||||
EXPECT_TRUE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCoreProfile());
|
||||
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user